├── spec ├── spec_command.rb ├── fixtures │ └── yard.opts ├── spec_api.rb ├── helper.rb └── spec_parser.rb ├── var ├── name ├── title ├── version ├── created ├── organizations ├── summary ├── authors ├── copyrights ├── repositories ├── description ├── requirements └── resources ├── Gemfile ├── work ├── sandbox │ ├── ex.rb │ └── .option └── NOTES.md ├── lib ├── dotopts.rb └── dotopts │ ├── api.rb │ ├── command.rb │ └── parser.rb ├── .travis.yml ├── demo ├── applique │ ├── require.rb │ └── battery.rb └── 09_battery │ ├── substitution.md │ ├── commands_only.md │ ├── profiles_complex.md │ ├── profiles_basic.md │ └── profiles_regex.md ├── .gitignore ├── .yardopts ├── .opts ├── Rakefile ├── etc └── cov.rb ├── MANIFEST.txt ├── .index ├── LICENSE.txt ├── HISTORY.md ├── README.md └── .gemspec /spec/spec_command.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/name: -------------------------------------------------------------------------------- 1 | dotopts 2 | -------------------------------------------------------------------------------- /var/title: -------------------------------------------------------------------------------- 1 | DotOpts 2 | -------------------------------------------------------------------------------- /var/version: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2013-01-23 2 | -------------------------------------------------------------------------------- /var/organizations: -------------------------------------------------------------------------------- 1 | --- 2 | - Rubyworks 3 | -------------------------------------------------------------------------------- /var/summary: -------------------------------------------------------------------------------- 1 | Automatic Arguments for Ruby 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /var/authors: -------------------------------------------------------------------------------- 1 | --- 2 | - trans 3 | -------------------------------------------------------------------------------- /work/sandbox/ex.rb: -------------------------------------------------------------------------------- 1 | # Class X 2 | class X 3 | end 4 | -------------------------------------------------------------------------------- /var/copyrights: -------------------------------------------------------------------------------- 1 | --- 2 | - 2013 Rubyworks (BSD-2-Clause) 3 | -------------------------------------------------------------------------------- /spec/fixtures/yard.opts: -------------------------------------------------------------------------------- 1 | yard 2 | --title "Big Title" 3 | 4 | -------------------------------------------------------------------------------- /work/sandbox/.option: -------------------------------------------------------------------------------- 1 | yard doc 2 | -p hero 3 | ex.rb 4 | -------------------------------------------------------------------------------- /lib/dotopts.rb: -------------------------------------------------------------------------------- 1 | require 'dotopts/api' 2 | 3 | DotOpts.configure! 4 | -------------------------------------------------------------------------------- /var/repositories: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git@github.com:rubyworks/dotopts.git 3 | -------------------------------------------------------------------------------- /var/description: -------------------------------------------------------------------------------- 1 | DotOpts is an automatic commandline argument augmenter for Ruby tools. 2 | -------------------------------------------------------------------------------- /var/requirements: -------------------------------------------------------------------------------- 1 | --- 2 | - qed (test) 3 | - spectroscope (test) 4 | - ae (test) 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.9.3 5 | - rbx-19mode 6 | - jruby-19mode 7 | 8 | -------------------------------------------------------------------------------- /demo/applique/require.rb: -------------------------------------------------------------------------------- 1 | # test assertions 2 | require 'ae' 3 | 4 | # what we are testing 5 | require 'dotopts/api' 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ergo/digest 2 | .yardoc/ 3 | doc/ 4 | log/ 5 | pkg/ 6 | tmp/ 7 | web/ 8 | /QED.md 9 | /Gemfile.lock 10 | *.gem 11 | 12 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title DotOpts 2 | --readme README.md 3 | --output-dir doc 4 | --protected 5 | --private 6 | lib 7 | - 8 | *.md 9 | *.txt 10 | 11 | -------------------------------------------------------------------------------- /var/resources: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/dotopts 3 | code: http://github.com/rubyworks/dotopts 4 | mail: http://groups.google.com/group/rubyworks-mailinglist 5 | 6 | -------------------------------------------------------------------------------- /.opts: -------------------------------------------------------------------------------- 1 | yardoc 2 | yard doc 3 | --title DotOpts 4 | --readme README.md 5 | --output-dir doc 6 | --protected 7 | --private 8 | lib 9 | - 10 | *.md 11 | *.txt 12 | 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | desc "Run demos." 4 | task "qed" do 5 | require 'qed' 6 | QED.run! 7 | end 8 | 9 | desc "Run demos with coverage report." 10 | task "qed:cov" do 11 | require 'qed' 12 | QED.run!('cov') do |c| 13 | require 'simplecov' 14 | SimpleCov.command_name 'QED' 15 | SimpleCov.start do 16 | coverage_dir 'log/coverage' 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /etc/cov.rb: -------------------------------------------------------------------------------- 1 | # Coverage Report 2 | # 3 | # $ rubytest -c etc/cov 4 | # 5 | Test.configure do |run| 6 | run.files << 'spec/**/spec_*.rb' 7 | run.requires << ['ae', 'spectroscope'] 8 | run.before do 9 | require 'simplecov' 10 | SimpleCov.command_name(File.basename($0)) 11 | SimpleCov.start do 12 | add_filter "spec/" 13 | coverage_dir 'log/coverage' 14 | end 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /demo/09_battery/substitution.md: -------------------------------------------------------------------------------- 1 | # Substituions Battery 2 | 3 | ## Most basic example 4 | 5 | Given a `.option` file: 6 | 7 | yard 8 | --title $title 9 | 10 | When we run `title=Title yard`, we should get the arguments: 11 | 12 | --title 13 | Title 14 | 15 | ## Environment profile substitutions 16 | 17 | Given a `.option` file: 18 | 19 | [a=$b] 20 | yard 21 | --title "New Title" 22 | 23 | When we run `a=1 b=1 yard`, we should get the arguments: 24 | 25 | --title 26 | New Title 27 | 28 | -------------------------------------------------------------------------------- /MANIFEST.txt: -------------------------------------------------------------------------------- 1 | #!mast .index .ruby .test .yaropts bin demo lib man spec test *.md *.txt 2 | .index 3 | demo/09_battery/commands_only.md 4 | demo/09_battery/profiles_basic.md 5 | demo/09_battery/profiles_complex.md 6 | demo/09_battery/profiles_regex.md 7 | demo/09_battery/substitution.md 8 | demo/applique/battery.rb 9 | demo/applique/require.rb 10 | lib/dotopts/api.rb 11 | lib/dotopts/command.rb 12 | lib/dotopts/parser.rb 13 | lib/dotopts.rb 14 | spec/fixtures/yard.opts 15 | spec/helper.rb 16 | spec/spec_api.rb 17 | spec/spec_command.rb 18 | spec/spec_parser.rb 19 | HISTORY.md 20 | README.md 21 | LICENSE.txt 22 | -------------------------------------------------------------------------------- /spec/spec_api.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | describe DotOpts do 4 | 5 | it "can configure!" do 6 | ENV['cmd'] = 'yard' 7 | ARGV.replace([]) 8 | 9 | example_avex_file = File.dirname(__FILE__) + '/fixtures/yard.opts' 10 | 11 | DotOpts.configure!(File.new(example_avex_file)) 12 | 13 | ARGV[-2].assert == '--title' 14 | ARGV[-1].assert == 'Big Title' 15 | end 16 | 17 | it "can should not configure! when arguments are given" do 18 | ENV['cmd'] = 'yard' 19 | ARGV.replace(['--private']) 20 | 21 | example_avex_file = File.dirname(__FILE__) + '/fixtures/yard.opts' 22 | 23 | DotOpts.configure!(File.new(example_avex_file)) 24 | 25 | ARGV[-1].assert == '--private' 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /demo/applique/battery.rb: -------------------------------------------------------------------------------- 1 | When "Given a `.option` file" do |text| 2 | ENV.replace({}) 3 | ARGV.replace([]) 4 | 5 | @opts_parser = nil 6 | @opts_text = text 7 | end 8 | 9 | When 'we run `/(.*?)/`' do |command| 10 | args = command.split(/\s+/) 11 | 12 | while (/=/ =~ args.first) 13 | name, value = args.shift.split('=') 14 | ENV[name] = value 15 | end 16 | 17 | ENV['cmd'] = args.shift 18 | ARGV.replace(args) 19 | 20 | DotOpts.configure!(@opts_text) 21 | end 22 | 23 | When 'we should get the arguments' do |text| 24 | #opts_args = [] 25 | #@opts_cmds.each do |c| 26 | # next unless c.current? 27 | # opts_args += c.arguments 28 | #end 29 | 30 | args = text.split("\n").map{ |x| x.strip } 31 | args.assert == ARGV #opts_args 32 | end 33 | 34 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['RUBYOPT'].to_s.include?('-rdotopts') 2 | abort "Remove `-rdotopts` from RUBYOPT environment first." 3 | end 4 | 5 | require 'spectroscope' 6 | require 'ae' 7 | 8 | #if ENV['cov'] 9 | # require 'simplecov' 10 | # SimpleCov.command_name File.basename($0) 11 | # SimpleCov.start do 12 | # add_filter "spec/" 13 | # coverage_dir 'log/coverage' 14 | # end 15 | #end 16 | 17 | require 'dotopts/api' 18 | 19 | module Kernel 20 | 21 | def with_env(hash) 22 | hold = {} 23 | hash.each do |name, value| 24 | name, value = name.to_s, value.to_s 25 | hold[name], ENV[name] = ENV[name], value 26 | end 27 | begin 28 | yield 29 | ensure 30 | hold.each do |name, value| 31 | ENV[name] = value 32 | end 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /demo/09_battery/commands_only.md: -------------------------------------------------------------------------------- 1 | # Example Battery 2 | 3 | ## Most basic example 4 | 5 | Given a `.option` file: 6 | 7 | yard 8 | --title "Big Title" 9 | 10 | When we run `yard`, we should get the arguments: 11 | 12 | --title 13 | Big Title 14 | 15 | ## Example with later matching commande 16 | 17 | Given a `.option` file with a number of profiles: 18 | 19 | something 20 | --title "Little Title" 21 | 22 | yard 23 | --title "Orange Title" 24 | 25 | When we run `yard` we should get the arguments: 26 | 27 | --title 28 | Orange Title 29 | 30 | ## Example with multiple matching commands 31 | 32 | Given a `.option` file with a number of commands: 33 | 34 | yard 35 | --title "First Title" 36 | 37 | something 38 | --title "Little Title" 39 | 40 | yard 41 | --output-dir "doc" 42 | 43 | When we run `yard` we should get the arguments: 44 | 45 | --title 46 | First Title 47 | --output-dir 48 | doc 49 | 50 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | revision: 2013 3 | type: ruby 4 | sources: 5 | - var 6 | authors: 7 | - name: trans 8 | email: transfire@gmail.com 9 | organizations: 10 | - name: Rubyworks 11 | requirements: 12 | - groups: 13 | - test 14 | development: true 15 | name: qed 16 | - groups: 17 | - test 18 | development: true 19 | name: spectroscope 20 | - groups: 21 | - test 22 | development: true 23 | name: ae 24 | conflicts: [] 25 | alternatives: [] 26 | resources: 27 | - type: home 28 | uri: http://rubyworks.github.com/dotopts 29 | label: Homepage 30 | - type: code 31 | uri: http://github.com/rubyworks/dotopts 32 | label: Source Code 33 | - type: mail 34 | uri: http://groups.google.com/group/rubyworks-mailinglist 35 | label: Mailing List 36 | repositories: 37 | - name: upstream 38 | scm: git 39 | uri: git@github.com:rubyworks/dotopts.git 40 | categories: [] 41 | copyrights: 42 | - holder: Rubyworks 43 | year: '2013' 44 | license: BSD-2-Clause 45 | customs: [] 46 | paths: 47 | lib: 48 | - lib 49 | created: '2013-01-23' 50 | summary: Automatic Arguments for Ruby 51 | title: DotOpts 52 | version: 0.2.0 53 | name: dotopts 54 | description: DotOpts is an automatic commandline argument augmenter for Ruby tools. 55 | date: '2013-02-16' 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD-2-Clause License 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 16 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 17 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 18 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 19 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 20 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | -------------------------------------------------------------------------------- /demo/09_battery/profiles_complex.md: -------------------------------------------------------------------------------- 1 | # Environment Profile Battery 2 | 3 | ## Basic environment profile 4 | 5 | Given a `.option` file: 6 | 7 | [a=1] 8 | yard 9 | --title "Sexy Title" 10 | 11 | When we run `a=1 yard`, we should get the arguments: 12 | 13 | --title 14 | Sexy Title 15 | 16 | ## Multi-matching profile 17 | 18 | Given a `.option` file: 19 | 20 | [a=1 b=2] 21 | yard 22 | --title "Soapy Title" 23 | 24 | When we run `a=1 b=2 yard`, we should get the arguments: 25 | 26 | --title 27 | Soapy Title 28 | 29 | ## Multiple multi-matching profiles 30 | 31 | Given a `.option` file: 32 | 33 | [a=1 b=1] 34 | yard 35 | --title "Untimely Title" 36 | 37 | [a=1 b=3] 38 | yard 39 | --title "Timely Title" 40 | 41 | When we run `a=1 b=3 yard`, we should get the arguments: 42 | 43 | --title 44 | Timely Title 45 | 46 | ## Multiple matching multi-matching profiles 47 | 48 | Given a `.option` file: 49 | 50 | [a=1 b=1] 51 | yard 52 | --title "Rocking Title" 53 | 54 | [a=1 b=3] 55 | yard 56 | --title "Hat Title" 57 | 58 | [a=1] 59 | yard 60 | --output-dir "doc" 61 | 62 | When we run `a=1 b=1 yard`, we should get the arguments: 63 | 64 | --title 65 | Rocking Title 66 | --output-dir 67 | doc 68 | 69 | -------------------------------------------------------------------------------- /demo/09_battery/profiles_basic.md: -------------------------------------------------------------------------------- 1 | # Examples with Just Profiles 2 | 3 | ## Basic example with multiple profiles 4 | 5 | Given a `.option` file: 6 | 7 | yard 8 | --title "Cool Title" 9 | 10 | [something] 11 | yard 12 | --title "Big Title" 13 | 14 | When we run `yard`, we should get the arguments: 15 | 16 | --title 17 | Cool Title 18 | 19 | ## Example with single profile 20 | 21 | Given a `.option` file with a leading profile: 22 | 23 | [example] 24 | yard 25 | --title "Funny Title" 26 | 27 | When we run `p=example yard`, we should get the arguments: 28 | 29 | --title 30 | Funny Title 31 | 32 | ## Example with later matching profile 33 | 34 | Given a `.option` file with a number of profiles: 35 | 36 | [something] 37 | yard 38 | --title "Little Title" 39 | 40 | [example] 41 | yard 42 | --title "Orange Title" 43 | 44 | When we run `p=example yard` we should get the arguments: 45 | 46 | --title 47 | Orange Title 48 | 49 | ## Example with multiple matching profiles 50 | 51 | Given a `.option` file with a number of profiles: 52 | 53 | [example] 54 | yard 55 | --title "First Title" 56 | 57 | [something] 58 | yard 59 | --title "Little Title" 60 | 61 | [example] 62 | yard 63 | --output-dir "doc" 64 | 65 | When we run `p=example yard` we should get the arguments: 66 | 67 | --title 68 | First Title 69 | --output-dir 70 | doc 71 | 72 | -------------------------------------------------------------------------------- /demo/09_battery/profiles_regex.md: -------------------------------------------------------------------------------- 1 | # Environment Profile Battery 2 | 3 | ## Basic profile regex 4 | 5 | Given a `.option` file: 6 | 7 | [~"q(x)?"] 8 | yard 9 | --title "Sexy Title" 10 | 11 | When we run `p=q yard`, we should get the arguments: 12 | 13 | --title 14 | Sexy Title 15 | 16 | And when we run `p=qx yard`, we should get the arguments: 17 | 18 | --title 19 | Sexy Title 20 | 21 | ## Basic environment profile regex 22 | 23 | Given a `.option` file: 24 | 25 | [a=~"x|y"] 26 | yard 27 | --title "Reggie Title" 28 | 29 | When we run `a=x yard`, we should get the arguments: 30 | 31 | --title 32 | Reggie Title 33 | 34 | And when we run `a=y yard`, we should get the arguments: 35 | 36 | --title 37 | Reggie Title 38 | 39 | ## Complex profile 40 | 41 | Given a `.option` file: 42 | 43 | [~"q(x)?" a=~"x|y"] 44 | yard 45 | --title "Complex Title" 46 | 47 | When we run `p=q a=x yard`, we should get the arguments: 48 | 49 | --title 50 | Complex Title 51 | 52 | When we run `p=q a=x yard`, we should get the arguments: 53 | 54 | --title 55 | Complex Title 56 | 57 | When we run `p=qx a=x yard`, we should get the arguments: 58 | 59 | --title 60 | Complex Title 61 | 62 | When we run `p=q a=y yard`, we should get the arguments: 63 | 64 | --title 65 | Complex Title 66 | 67 | When we run `p=qx a=y yard`, we should get the arguments: 68 | 69 | --title 70 | Complex Title 71 | 72 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # RELEASE HISTORY 2 | 3 | ## 0.2.0 / 2013-02-16 4 | 5 | Rethought overall design and reimplemented parser. The syntax of option 6 | files is essentially the same, but now arguments are only applied if 7 | there are no arguments provided on the command line. In other words, 8 | it is assumed that if the user supplies their own arguments, then 9 | they don't require any from DotOpts. This redesign prevents a lot of 10 | potential headaches with how prepend/append options can interact 11 | with options proved by the end user. In the future, we might adds some 12 | additional flexibility, of argument substitutions. 13 | 14 | Changes: 15 | 16 | * Rethink when arguments are applied. 17 | * Reimplemented parser. 18 | 19 | 20 | ## 0.1.3 / 2013-01-30 21 | 22 | Environment settings were not being applied. This release improves the code 23 | that handles application of options and fixes the environment setting issue. 24 | This release also adds a debug log option, so one can see what DotOpts is doing 25 | by setting `dotopts_debug`. 26 | 27 | Changes: 28 | 29 | * Fix environment setting, ensuring their application. 30 | * Add debug log when `doptopts_debug` is set. 31 | 32 | 33 | ## 0.1.2 / 2013-01-29 34 | 35 | Accidental release. Yanked. 36 | 37 | 38 | ## 0.1.1 / 2013-01-29 39 | 40 | This release simply fixes the missing optional argument on the 41 | `DotOpts#configure!` method's interface. 42 | 43 | Changes: 44 | 45 | * Fix `DotOpts#configure!` method. [bug] 46 | 47 | 48 | ## 0.1.0 / 2013-01-28 49 | 50 | First release of DotOpts. 51 | 52 | Changes: 53 | 54 | * It's Your Birthday! 55 | 56 | -------------------------------------------------------------------------------- /spec/spec_parser.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | describe DotOpts::Parser do 4 | 5 | #before do 6 | # ENV['cmd'] = 'yard' # FIXME: try `foo` and watch what happens 7 | #end 8 | 9 | describe "without profiles" do 10 | it "can parse configuration" do 11 | text = "yard\n" + 12 | " --title Super" 13 | cmds = DotOpts::Parser.parse(text) 14 | 15 | cmds.size.assert == 1 16 | cmds[0].name.assert == 'yard' 17 | cmds[0].arguments.assert == ['--title', 'Super'] 18 | cmds[0].profile.assert.nil? 19 | end 20 | 21 | it "can parse configuration with multiple arguments" do 22 | text = "yard\n" + 23 | " --title Super\n" + 24 | " --private\n" 25 | cmds = DotOpts::Parser.parse(text) 26 | 27 | cmds.size.assert == 1 28 | cmds[0].name.assert == 'yard' 29 | cmds[0].arguments.assert == ['--title', 'Super', '--private'] 30 | cmds[0].profile.assert.nil? 31 | end 32 | 33 | it "can parse configuration with multiple commands" do 34 | text = "yard\n" + 35 | " --title Super\n" + 36 | " --private\n" + 37 | "rdoc\n" + 38 | " --private\n" 39 | cmds = DotOpts::Parser.parse(text) 40 | 41 | cmds.size.assert == 2 42 | 43 | cmds[0].name.assert == 'yard' 44 | cmds[0].arguments.assert == ['--title', 'Super', '--private'] 45 | cmds[0].profile.assert.nil? 46 | 47 | cmds[1].name.assert == 'rdoc' 48 | cmds[1].arguments.assert == ['--private'] 49 | cmds[1].profile.assert.nil? 50 | end 51 | end 52 | 53 | describe "with profiles" do 54 | it "can parse configuration with initial profile" do 55 | text = "[example]\n" + 56 | "yard\n" + 57 | " --title Super" 58 | cmds = DotOpts::Parser.parse(text) 59 | 60 | cmds.size.assert == 1 61 | cmds.first.name.assert == 'yard' 62 | cmds.first.profile.assert == 'example' 63 | cmds.first.arguments.assert == ['--title', 'Super'] 64 | end 65 | 66 | it "can parse configuration with multiple profiles" do 67 | text = "[something]\n" + 68 | "yard\n" + 69 | " --title Sucky\n" + 70 | "\n" + 71 | "[example]\n" + 72 | "yard\n" + 73 | " --title Super" 74 | 75 | cmds = DotOpts::Parser.parse(text) 76 | 77 | cmds.size.assert == 2 78 | 79 | cmds[0].name.assert == 'yard' 80 | cmds[0].profile.assert == 'something' 81 | cmds[0].arguments.assert == ['--title', 'Sucky'] 82 | 83 | cmds[1].name.assert == 'yard' 84 | cmds[1].profile.assert == 'example' 85 | cmds[1].arguments.assert == ['--title', 'Super'] 86 | end 87 | end 88 | 89 | #after do 90 | # ENV['cmd'] = nil 91 | #end 92 | 93 | end 94 | 95 | -------------------------------------------------------------------------------- /lib/dotopts/api.rb: -------------------------------------------------------------------------------- 1 | module DotOpts 2 | require 'dotopts/parser' 3 | 4 | # Configuration file names. 5 | # 6 | # @note Sorry, I could not decide between these two. 7 | OPTIONS_FILES = ['.opts', '.option'] 8 | 9 | # Configure 10 | # 11 | # @param [String] file 12 | # The configuration file to load. (optional) 13 | # 14 | # @return [void] 15 | def self.configure!(io=nil) 16 | text, file = read_config(io) 17 | 18 | if text 19 | cmds = Parser.parse(text) 20 | 21 | applicable = cmds.select do |c| 22 | c.current? 23 | end 24 | 25 | applicable.each do |c| 26 | argv = c.arguments 27 | env = c.environment 28 | 29 | debug(file, argv, env) 30 | apply(argv, env) 31 | end 32 | end 33 | end 34 | 35 | # Returns the options file of the current project. 36 | # 37 | # @return [String] The options file of the project. 38 | def self.options_file 39 | if project_root 40 | OPTIONS_FILES.each do |optfile| 41 | file = File.join(project_root, optfile) 42 | return file if File.exist?(file) 43 | end 44 | end 45 | end 46 | 47 | # Find the root directory of the current project. 48 | # 49 | # @return [String,nil] The root directory of the project. 50 | def self.project_root(start_dir=Dir.pwd) 51 | dir = start_dir 52 | home = File.expand_path('~') 53 | until dir == home || dir == '/' 54 | OPTIONS_FILES.each do |optfile| 55 | if File.exist?(File.join(dir, optfile)) 56 | return dir 57 | end 58 | end 59 | dir = File.dirname(dir) 60 | end 61 | nil 62 | end 63 | 64 | # Apply arguments and environment options. 65 | # 66 | # @return [void] 67 | def self.apply(argv, env={}) 68 | env.each{ |k,v| ENV[k.to_s] = v.to_s } 69 | ARGV.concat(argv) 70 | #ARGV.replace(argv + ARGV) 71 | end 72 | 73 | # Print message to stderr if dopts_debug flag it set. 74 | # 75 | # @return [void] 76 | def self.debug(file, argv, env) 77 | return unless ENV['dotopts_debug'] 78 | 79 | $stderr.puts "dotopts file: #{file}" 80 | 81 | unless argv.empty? 82 | msg = argv.join(' ') 83 | $stderr.puts "dotopts argv: " + msg 84 | end 85 | 86 | unless env.empty? 87 | msg = env.map{ |k,v| "#{k}=#{v.inspect}" }.join(' ') 88 | $stderr.puts "dotopts env: " + msg 89 | end 90 | end 91 | 92 | # Take an IO object and read it in. If it is a File 93 | # object also return the file name. Strings are passed 94 | # through untouched. 95 | # 96 | # @return [Array] 97 | def self.read_config(io) 98 | text, file = nil, '(io)' 99 | 100 | case io 101 | when String 102 | text = io 103 | when File 104 | text = io.read 105 | file = io.path 106 | when nil 107 | if file = options_file 108 | text = File.read(file) 109 | end 110 | else 111 | text = io.read 112 | end 113 | 114 | return text, file 115 | end 116 | 117 | end 118 | -------------------------------------------------------------------------------- /work/NOTES.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | ## 2013-02-15 Simplify the Scope 4 | 5 | I gave the previous concerns some deep consideration, and after a good nights 6 | sleep I believe I have come to a satisfactorey conclusion. 7 | 8 | It is enough to simple support special "cases". The default case being 9 | when no arguments are given for a command. Profiles provide for additonal 10 | cases in the same vein. The only other case we might support is substitution. 11 | In in that case, if a particular argument is given then it will be replaced 12 | by others. We might mark that up something like: 13 | 14 | ``` 15 | yardoc 16 | yard doc 17 | --title 'My Docs' 18 | 19 | --one-file 20 | ``` 21 | 22 | So then if `$ yard doc minidoc` is used, the would replace `minidoc` with 23 | `--one-file`. This brings with it a bit of complexity of whether to include 24 | the non-subsitute arguments with the substitute arguments. But other than 25 | that is quite straight forward. 26 | 27 | 28 | ## 2013-02-14 More Flexibility Is Needed 29 | 30 | Well, it turns out that just appending arguments is not going to cut it. 31 | Prepending would be better, but its still not perfect as some options 32 | are concatitive, rather than replacing. 33 | 34 | In all, there would seem to be a number of possible manipulations 35 | someone might want to make: 36 | 37 | * Default arguments (when no others are provided) 38 | * Append arguments 39 | * Prepend arguments 40 | * Remove arguments 41 | * Replace arguments 42 | * Append arguments conditionally 43 | * Prepend arguments conditionally 44 | * Remove arguments conditionally 45 | * Default non-option arguments (when no non-option arguments are provided) 46 | 47 | Of course we could get as complex as any programming language, but that 48 | about covers the vast majority of use-cases. The first four are the most 49 | basic and most common, and if we supported only those it would suffice. 50 | The last five are all conditional and a bit more complex. The last in 51 | particular has a caveat in that it is not possible to be 100% sure if 52 | a command has non-option arguments or not, b/c some options take arguments 53 | themselves. 54 | 55 | I worked out one possible notation, but I can't say I find it all that 56 | wondeful: 57 | 58 | ``` 59 | yardoc 60 | yard doc 61 | $ foo=100 62 | + lib 63 | - 64 | *.md 65 | *.txt 66 | > --output-dir doc 67 | < --readme README.md 68 | --title DotOpts 69 | --protected 70 | ! --private ? 71 | ``` 72 | 73 | Basically, the key is as follows: 74 | 75 | * `$` - environment variables 76 | * `+` - defaults 77 | * `>` - prepend 78 | * `<` - append 79 | * `!` - remove 80 | 81 | Then again, given how esoteric that is, maybe we should just spell it out 82 | instead. 83 | 84 | ``` 85 | yardoc 86 | yard doc 87 | ENVIRONMENT 88 | foo=100 89 | DEFAULT 90 | lib 91 | - 92 | *.md 93 | *.txt 94 | PREPEND 95 | --output-dir doc 96 | APPEND 97 | --readme README.md 98 | --title DotOpts 99 | --protected 100 | REMOVE 101 | --private ? 102 | ``` 103 | 104 | I was hoping to avoid that, but it looks like we are going to ultimately 105 | need this level of flexibility, and unless there is some other genius notation 106 | then this seems the clearest approach. It easily allows for other forms of 107 | manipulation just be adding a named section and a bit of special notation. 108 | 109 | ``` 110 | REPLACE 111 | --private | --public 112 | APPEND_IF 113 | --private ? --foo : --bar 114 | ``` 115 | 116 | My only other thought at this point, is perhaps something more sed would be 117 | useful. Sigh, I guess I really am getting the point of having to implement 118 | a mini-programming language just for manipulaing argument lists. 119 | 120 | ``` 121 | yardoc 122 | yard doc 123 | environment(foo=100) 124 | default(lib - *.md *.txt) 125 | prepend(--output-dir doc) 126 | append(--readme README.md --title DotOpts --protected) 127 | remove(--private) 128 | if(--private) { 129 | remove(--private) 130 | prepend(--public) 131 | } 132 | ``` 133 | 134 | Ugh. Do we really need to go there? 135 | 136 | -------------------------------------------------------------------------------- /lib/dotopts/command.rb: -------------------------------------------------------------------------------- 1 | module DotOpts 2 | 3 | # Get the current command. 4 | # 5 | # @note This is take from basename of `$0`. In the future, we may 6 | # need to find a way to tweak this to somehow include parrent 7 | # directories. 8 | # 9 | # @todo Is ENV['cmd'] okay? Maybe ['dotopts_command'] would be better? 10 | def self.command 11 | ENV['cmd'] || File.basename($0) 12 | end 13 | 14 | ## 15 | # Command class encapsulate a configuration for a given command and 16 | # a given profile. 17 | # 18 | class Command 19 | 20 | # Initialize new instance of Command. 21 | # 22 | # @param [String] name 23 | # The name of the command. Can include subcommand, e.g. `yard doc`. 24 | # 25 | # @option settings [String,nil] :profile 26 | # The profile for which this command configuation would be applicable. 27 | # 28 | # @return [void] 29 | def initialize(name, settings={}) 30 | @name = name 31 | 32 | self.profile = settings[:profile] 33 | 34 | @arguments = [] 35 | @environment = {} 36 | end 37 | 38 | # Command name. [String] 39 | attr :name 40 | 41 | # Profile designation. 42 | # 43 | # @return [String,nil] 44 | def profile 45 | @profile 46 | end 47 | 48 | # Set profile designation. 49 | # 50 | # @param [String,nil] 51 | # The profile designation. 52 | # 53 | # @return [String,nil] 54 | def profile=(profile) 55 | @profile = profile ? profile.to_str : nil #? shellwords(profile).first : nil 56 | end 57 | 58 | # Environment settings. 59 | # 60 | # @return [Hash] 61 | def environment 62 | @environment 63 | end 64 | 65 | # Arguments. 66 | # 67 | # @return [Array] 68 | def arguments 69 | @arguments 70 | end 71 | 72 | # Add argument or environment entries to command. 73 | # 74 | # TODO: Is there too much "parsing" going on here? 75 | # Should some of this be in Parser instead? 76 | def <<(entry) 77 | entry = entry.strip 78 | if entry.start_with?('$ ') 79 | # environment 80 | entry.sub!(/\$\s+/, '') 81 | shellwords(entry).each do |s| 82 | name, value = s.split('=') 83 | @environment[name] = subenv(value) unless name.empty? 84 | end 85 | else 86 | # argument 87 | shellwords(entry).each do |s| 88 | @arguments << subenv(s) unless s.empty? 89 | end 90 | end 91 | end 92 | 93 | # Is the command applicable to the current command line? 94 | # 95 | # @return [Boolean] 96 | def current? 97 | command? && profile? 98 | end 99 | 100 | # Does the command's name match the current command? 101 | # 102 | # @return [Boolean] 103 | def command? 104 | this = @name.split(' ') 105 | real = [DotOpts.command, *ARGV][0,this.size] 106 | this == real && ARGV.size < this.size # no other arguments 107 | end 108 | 109 | # Does the command's profile match the current environment? 110 | # 111 | # @return [Boolean] 112 | def profile? 113 | shellwords(profile || "").all? do |sw| 114 | case sw 115 | when /^\~/ 116 | true if Regexp.new(sw.sub('~','')) === (ENV['profile'] || ENV['p']).to_s 117 | when /=~/ 118 | name, value = sw.split('=~') 119 | #name = 'profile' if name.empty? 120 | true if Regexp.new(value) === ENV[name] 121 | when /=/ 122 | name, value = sw.split('=') 123 | #name = 'profile' if name.empty? 124 | true if subenv(value) == ENV[name] 125 | else 126 | true if sw.to_s == (ENV['profile'] || ENV['p']).to_s 127 | end 128 | end 129 | end 130 | 131 | private 132 | 133 | # Split a string up into shellwords. 134 | # 135 | # @return [Array] 136 | def shellwords(value) 137 | Shellwords.shellwords(value) 138 | end 139 | 140 | # Substitute environment variables. 141 | # 142 | # @return [String] 143 | def subenv(value) 144 | value.gsub(/\$(\w+)/){ |m| ENV[$1] } 145 | end 146 | 147 | end 148 | 149 | end 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotOpts 2 | 3 | **Automated Command-line Options (for Ruby Executables)** 4 | 5 | [Website](http://rubyworks.github.com/dotopts) / 6 | [Report Issue](http://github.com/rubyworks/dotopts/issues) / 7 | [Source Code](http://github.com/rubyworks/dotopts) / 8 | [![Build Status](https://secure.travis-ci.org/rubyworks/dotopts.png)](http://travis-ci.org/rubyworks/dotopts) / 9 | [![Gem Version](https://badge.fury.io/rb/dotopts.png)](http://badge.fury.io/rb/dotopts) 10 | 11 | ## About 12 | 13 | DotOpts is an automatic command-line argument augmenter. It looks for a 14 | project's local `.option` (or `.opts`) configuration file and applies the 15 | appropriate arguments when a matching command is invoked. 16 | 17 | 18 | ## Features 19 | 20 | * Works with any and all Ruby-based executables. 21 | * Can be used to set environment variables in addition to arguments. 22 | * Supports environment variable substitution. 23 | * Supports conditional augmentation using environment settings. 24 | * Simple and easy to understand plain-text configuration format. 25 | 26 | 27 | ## Install 28 | 29 | If you are using an application that depends on DotOpts for configuration, 30 | there is nothing you have to do. Installing the said application via 31 | RubyGems should also install DotOpts and require it as needed. 32 | 33 | ### General Setup 34 | 35 | To use DotOpts universally, even for command-line applications that do not 36 | directly utilize it, you can install DotOpts via RubyGems: 37 | 38 | gem install dotopts 39 | 40 | Then add `-rdotopts` to your `RUBYOPT` environment variable. 41 | 42 | export RUBYOPT="-rdotopts" 43 | 44 | This ensures DotOpts is used whenever Ruby is used. 45 | 46 | ### Special Setup 47 | 48 | Another approach is to use DotOpts per-project development project using 49 | via Bundler, adding DotOpts to your project's Gemfile. 50 | 51 | gem 'dotopts' 52 | 53 | This will allow DotOpts to work whenever using `bundle exec` or Bundler 54 | created binstub. 55 | 56 | 57 | ## Usage 58 | 59 | ### Setting Arguments 60 | 61 | A simple example of a projects `.option` file: 62 | 63 | yardoc 64 | yard doc 65 | --title="Bad Ass Program" 66 | 67 | This simply says, that whenever `yardoc` or `yard doc` is executed, and 68 | no other arguments are given, then add the `--title="Bad Ass Program"` 69 | argument to the end of the command's arguments (internally `ARGV`). 70 | 71 | 72 | ### Setting Environment Variables 73 | 74 | Environment variables can also be set by prepending first and subsequent 75 | lines with `$ `. 76 | 77 | yardoc 78 | yard doc 79 | $ RUBYOPT="-rbadass" 80 | --title="Bad Ass Program" 81 | 82 | The space after the cash sign is important! Otherwise it will be interpreted 83 | as a variable substitution. 84 | 85 | 86 | ### Conditional Profiles 87 | 88 | The `.option` configuration file supports profiles via the square brackets. 89 | Profiles are chosen via the `$profile` or `$p` environment variable. 90 | 91 | ``` 92 | [coverage] 93 | rubytest 94 | -r microtest 95 | ``` 96 | 97 | So the above means that `-r micortest` should be added the argument list when 98 | `rubytest` is executed, but only if `$profile` or `$p` is equal to `"coverage"`. 99 | 100 | Square brackets can also be used to match against any environment variable 101 | by using the `=` sign. 102 | 103 | ``` 104 | [RUBY_ENGINE=jruby] 105 | rake test 106 | -r jruby-sandbox 107 | ``` 108 | 109 | To condition a configuration on multiple environment settings, add each 110 | to the square brackets separated by a space. 111 | 112 | ``` 113 | [coverage RUBY_ENGINE=jruby] 114 | rubytest 115 | -r jruby-sandbox 116 | -r microtest 117 | ``` 118 | 119 | Finally, environment values can be matched against simple regular expressions 120 | using a tilde (`~`) before the value. Be sure to put the value in quotes when 121 | using regular expressions. 122 | 123 | ``` 124 | [~"cov(erage)?" RUBY_ENGINE=~"jruby|rubinius"] 125 | rubytest 126 | -r jruby-sandbox 127 | -r microtest 128 | ``` 129 | 130 | ### Tool Support 131 | 132 | Ruby tool developers can support DotOpts out-of-the-box simple by running 133 | `require 'dotopts'` in their program before parsing ARGV. DotOpts 134 | simply injects arguments into `ARGV` so it can work with any command-line 135 | options parser. 136 | 137 | 138 | ## Development 139 | 140 | ### Suggestions & Contributions 141 | 142 | DotOpts is a brand new application, and still rather wet behind the ears, so to 143 | speak. So your input is critical to making it better. Any and all suggestions and 144 | contributions are much appreciated. If you have any ideas on how to improve DotOpts, 145 | or find any flaws in its design that need address, please drop a comment on the 146 | [Issues](http://github.com/rubyworks/dotopts/issues) page. Or even better, be proactive! 147 | Fork the project and submit a pull request. Thanks. 148 | 149 | ### Universal Solution? 150 | 151 | It would be awesome if it were possible to have DotOpts apply to *all* executables, 152 | not just Ruby-based executables. But I do not know how this can be done for Bash, Zsh 153 | or any other shell. Of course, each scripting language could potentially have 154 | its own implementation of DotOpts, which would cover many more executables, but it 155 | would still not cover all of them. 156 | 157 | If you are a shell genius and have an epiphany on how it might be done, please 158 | drop me a note via [Issues](http://github.com/rubyworks/dotopts/issues). I'd be more 159 | than happy to code and maintain it. 160 | 161 | 162 | ## Copyrights & Licensing 163 | 164 | DotOpts is copyrighted open-source software. 165 | 166 | *Copyright (c) 2013 Rubyworks. All rights reserved.* 167 | 168 | It can be modified and redistributed in accordance with the [BSD-2-Clause](http://spdex.org/licenses/bsd-2-clause) license. 169 | 170 | -------------------------------------------------------------------------------- /lib/dotopts/parser.rb: -------------------------------------------------------------------------------- 1 | module DotOpts 2 | 3 | class Parser 4 | require 'shellwords' 5 | require_relative 'command' 6 | 7 | # Regular expression to match profile headers. 8 | RE_PROFILE = /^\[(.*)\]/ 9 | 10 | # Regular expression to match command headers. 11 | RE_COMMAND = /^\w/ 12 | 13 | # Regular expression to match arguments. 14 | RE_ARGUMENT = /^\s+\S+/ 15 | 16 | # Regular expression to match environment setting. 17 | RE_ENVIRONMENT = /^\s+\$/ 18 | 19 | # Regular expression to match blank strings. 20 | RE_BLANK = /^\s*$/ 21 | 22 | # Convenience constructor for `new(text).parse`. 23 | # 24 | # @return [Parser] 25 | def self.parse(text) 26 | parser = new(text) 27 | parser.parse 28 | parser.commands 29 | end 30 | 31 | # Initialize new instance. 32 | # 33 | # @param [#to_s] text 34 | # 35 | def initialize(text) 36 | @text = text.to_s 37 | 38 | # 39 | @commands = [] 40 | 41 | # Holds the current commands being parsed. 42 | @_commands = [] 43 | @_profiles = [] 44 | end 45 | 46 | # 47 | attr :commands 48 | 49 | # The configuration document text. [String] 50 | attr :text 51 | 52 | # Parse the configuration text. 53 | # 54 | # 55 | def parse 56 | lines = @text.lines.to_a 57 | 58 | remove_blanks(lines) 59 | 60 | # put initial non-profiled settings last 61 | #if lines.first !~ RE_PROFILE 62 | # index = lines.index{ |line| line =~ RE_PROFILE } 63 | # if index 64 | # lines = lines[index..-1] + ['[]'] + lines[0...index] 65 | # else 66 | # #lines = ['[]'] + lines 67 | # end 68 | #end 69 | 70 | parse_profiles(lines) 71 | end 72 | 73 | # 74 | # 75 | # 76 | def parse_profiles(lines) 77 | @_profiles = [] 78 | until lines.empty? 79 | line = lines.first.rstrip 80 | case line 81 | when RE_BLANK 82 | lines.shift 83 | when RE_PROFILE 84 | @_profiles << $1 85 | lines.shift 86 | else 87 | #@_commands = [] 88 | parse_command(lines) 89 | end 90 | end 91 | end 92 | 93 | # Parse lines from command onward until another profile 94 | # or end of document is reached. 95 | # 96 | # @return [void] 97 | def parse_command(lines) 98 | previous = nil 99 | while line = lines.first 100 | case line 101 | when RE_BLANK 102 | when RE_COMMAND 103 | if previous != :command 104 | @commands.concat @_commands 105 | @_commands = [] 106 | end 107 | if @_profiles.empty? 108 | @_commands << Command.new(line.strip, :profile=>nil) 109 | else 110 | @_profiles.each do |profile| 111 | @_commands << Command.new(line.strip, :profile=>profile) 112 | end 113 | end 114 | previous = :command 115 | when RE_ARGUMENT, RE_ENVIRONMENT 116 | if @_commands.empty? 117 | raise SyntaxError, "no command before arguments\n@ #{line}" 118 | end 119 | @_commands.each{ |c| c << line } 120 | previous = :argument 121 | when RE_PROFILE 122 | @commands.concat @_commands 123 | @_commands = [] 124 | @_profiles = [] 125 | return 126 | end 127 | lines.shift 128 | end 129 | @commands.concat @_commands 130 | end 131 | 132 | # Remove intialize blank lines for an array of strings. 133 | # 134 | # @param [Array] lines 135 | # 136 | # @return [Array] 137 | def remove_blanks(lines) 138 | lines.shift while RE_BLANK =~ lines.first 139 | end 140 | 141 | end 142 | 143 | end 144 | 145 | =begin 146 | # Parse profiles. 147 | def parse_profiles(lines) 148 | until lines.empty? 149 | line = lines.first.rstrip 150 | if md = RE_PROFILE_HEADER.match(line) # TODO: this is a bit wanky 151 | profile = md.post_match.chomp(']') 152 | matches = shellwords(profile).all? do |shellword| 153 | case shellword 154 | when /^\~/ 155 | true if Regexp.new(shellword.sub('~','')) === (ENV['profile'] || ENV['p']).to_s 156 | when /=~/ 157 | name, value = shellword.split('=~') 158 | #name = 'profile' if name.empty? 159 | true if Regexp.new(value) === ENV[name] 160 | when /=/ 161 | name, value = shellword.split('=') 162 | #name = 'profile' if name.empty? 163 | true if subenv(value) == ENV[name] 164 | else 165 | true if shellword == (ENV['profile'] || ENV['p']).to_s 166 | end 167 | end 168 | 169 | if matches 170 | lines.shift while RE_PROFILE_HEADER =~ lines.first 171 | lines.shift while RE_BLANK_STRING =~ lines.first 172 | parse_commands(lines) 173 | end 174 | end 175 | lines.shift 176 | end 177 | end 178 | 179 | # Parse commands. 180 | def parse_commands(lines) 181 | while line = lines.first 182 | line = line.strip 183 | if md = RE_COMMAND_HEADER.match(line) 184 | if current_command == line 185 | lines.shift while RE_COMMAND_HEADER =~ lines.first 186 | lines.shift while RE_BLANK_STRING =~ lines.first 187 | parse_environment(lines) 188 | parse_arguments(lines) 189 | next 190 | end 191 | elsif RE_PROFILE_HEADER.match(line) 192 | return 193 | end 194 | lines.shift 195 | end 196 | end 197 | 198 | # Parse environment. 199 | def parse_environment(lines) 200 | while line = lines.first 201 | line = line.strip 202 | next if line.empty? 203 | break unless line.start_with?('$ ') 204 | line = line.sub(/\$\s+/, '') 205 | shellwords(line).each do |s| 206 | name, value = s.split('=') 207 | @environment[name] = subenv(value) unless name.empty? 208 | end 209 | lines.shift 210 | end 211 | end 212 | 213 | # Parse arguments. 214 | def parse_arguments(lines) 215 | while line = lines.first 216 | line = line.rstrip 217 | break if RE_PROFILE_HEADER =~ line 218 | break if RE_COMMAND_HEADER =~ line 219 | shellwords(line).each do |s| 220 | @arguments << subenv(s) unless s.empty? 221 | end 222 | lines.shift 223 | end 224 | end 225 | 226 | # The current command comes from the basename of `$0`. 227 | # But it can be overriddne by setting the `cmd` environment variable. 228 | # 229 | # @return [String] 230 | def current_command 231 | ENV['cmd'] || File.basename($0) 232 | end 233 | end 234 | 235 | end 236 | 237 | =end 238 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec 308 | --------------------------------------------------------------------------------