monkeyshines 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. data/.document +4 -0
  2. data/.gitignore +43 -0
  3. data/LICENSE +20 -0
  4. data/LICENSE.textile +20 -0
  5. data/README.textile +125 -0
  6. data/Rakefile +105 -0
  7. data/VERSION +1 -0
  8. data/examples/.gitignore +4 -0
  9. data/examples/bulk_urls/scrape_bulk_urls.rb +64 -0
  10. data/examples/rename_tree/rename_hdp_tree.rb +151 -0
  11. data/examples/rename_tree/rename_ripd_tree.rb +82 -0
  12. data/examples/rss_feeds/scrape_rss_feeds.rb +52 -0
  13. data/examples/shorturls/README.textile +111 -0
  14. data/examples/shorturls/bulkdump_shorturls.rb +46 -0
  15. data/examples/shorturls/bulkload_shorturls.rb +45 -0
  16. data/examples/shorturls/extract_urls.rb +12 -0
  17. data/examples/shorturls/multiplex_shorturl_cache.rb +32 -0
  18. data/examples/shorturls/old/multidump_and_fix_shorturls.rb +66 -0
  19. data/examples/shorturls/old/shorturl_stats.rb +81 -0
  20. data/examples/shorturls/scrape_shorturls.rb +112 -0
  21. data/examples/shorturls/shorturl_request.rb +29 -0
  22. data/examples/shorturls/shorturl_sequence.rb +121 -0
  23. data/examples/shorturls/shorturl_start_tyrant.sh +16 -0
  24. data/examples/shorturls/start_shorturl_cache.sh +2 -0
  25. data/lib/monkeyshines.rb +31 -0
  26. data/lib/monkeyshines/extensions.rb +16 -0
  27. data/lib/monkeyshines/fetcher.rb +10 -0
  28. data/lib/monkeyshines/fetcher/authed_http_fetcher.rb +35 -0
  29. data/lib/monkeyshines/fetcher/base.rb +44 -0
  30. data/lib/monkeyshines/fetcher/fake_fetcher.rb +19 -0
  31. data/lib/monkeyshines/fetcher/http_fetcher.rb +127 -0
  32. data/lib/monkeyshines/fetcher/http_head_fetcher.rb +23 -0
  33. data/lib/monkeyshines/monitor.rb +7 -0
  34. data/lib/monkeyshines/monitor/chunked_store.rb +23 -0
  35. data/lib/monkeyshines/monitor/periodic_logger.rb +33 -0
  36. data/lib/monkeyshines/monitor/periodic_monitor.rb +65 -0
  37. data/lib/monkeyshines/options.rb +59 -0
  38. data/lib/monkeyshines/recursive_runner.rb +26 -0
  39. data/lib/monkeyshines/repository/base.rb +57 -0
  40. data/lib/monkeyshines/repository/s3.rb +169 -0
  41. data/lib/monkeyshines/request_stream.rb +11 -0
  42. data/lib/monkeyshines/request_stream/base.rb +32 -0
  43. data/lib/monkeyshines/request_stream/edamame_queue.rb +54 -0
  44. data/lib/monkeyshines/request_stream/klass_request_stream.rb +39 -0
  45. data/lib/monkeyshines/request_stream/simple_request_stream.rb +22 -0
  46. data/lib/monkeyshines/runner.rb +161 -0
  47. data/lib/monkeyshines/runner_core/options.rb +5 -0
  48. data/lib/monkeyshines/runner_core/parsing_runner.rb +29 -0
  49. data/lib/monkeyshines/scrape_job/old_paginated.rb +343 -0
  50. data/lib/monkeyshines/scrape_job/recursive.rb +9 -0
  51. data/lib/monkeyshines/scrape_request.rb +136 -0
  52. data/lib/monkeyshines/scrape_request/paginated.rb +290 -0
  53. data/lib/monkeyshines/scrape_request/raw_json_contents.rb +16 -0
  54. data/lib/monkeyshines/scrape_request/signed_url.rb +86 -0
  55. data/lib/monkeyshines/store.rb +14 -0
  56. data/lib/monkeyshines/store/base.rb +29 -0
  57. data/lib/monkeyshines/store/chunked_flat_file_store.rb +37 -0
  58. data/lib/monkeyshines/store/conditional_store.rb +57 -0
  59. data/lib/monkeyshines/store/factory.rb +8 -0
  60. data/lib/monkeyshines/store/flat_file_store.rb +84 -0
  61. data/lib/monkeyshines/store/key_store.rb +51 -0
  62. data/lib/monkeyshines/store/null_store.rb +15 -0
  63. data/lib/monkeyshines/store/read_thru_store.rb +22 -0
  64. data/lib/monkeyshines/store/tokyo_tdb_key_store.rb +33 -0
  65. data/lib/monkeyshines/store/tyrant_rdb_key_store.rb +56 -0
  66. data/lib/monkeyshines/store/tyrant_tdb_key_store.rb +20 -0
  67. data/lib/monkeyshines/utils/factory_module.rb +106 -0
  68. data/lib/monkeyshines/utils/filename_pattern.rb +134 -0
  69. data/lib/monkeyshines/utils/logger.rb +15 -0
  70. data/lib/monkeyshines/utils/trollop-1.14/FAQ.txt +84 -0
  71. data/lib/monkeyshines/utils/trollop-1.14/History.txt +101 -0
  72. data/lib/monkeyshines/utils/trollop-1.14/Manifest.txt +7 -0
  73. data/lib/monkeyshines/utils/trollop-1.14/README.txt +40 -0
  74. data/lib/monkeyshines/utils/trollop-1.14/Rakefile +36 -0
  75. data/lib/monkeyshines/utils/trollop-1.14/lib/trollop.rb +744 -0
  76. data/lib/monkeyshines/utils/trollop-1.14/test/test_trollop.rb +1048 -0
  77. data/lib/monkeyshines/utils/trollop.rb +744 -0
  78. data/lib/monkeyshines/utils/union_interval.rb +52 -0
  79. data/lib/monkeyshines/utils/uri.rb +70 -0
  80. data/lib/monkeyshines/utils/uuid.rb +32 -0
  81. data/monkeyshines.gemspec +147 -0
  82. data/scrape_from_file.rb +44 -0
  83. data/spec/monkeyshines_spec.rb +7 -0
  84. data/spec/spec_helper.rb +9 -0
  85. metadata +183 -0
@@ -0,0 +1,134 @@
1
+ module Monkeyshines
2
+ module Utils
3
+ class FilenamePattern
4
+ # Memoize recognizer regexps
5
+ RECOGNIZER_REGEXPS = {}
6
+ # the filename pattern, e.g. 'ripd/:handle/:date/:handle+:timestamp-:pid-:hostname.tsv'
7
+ attr_accessor :pattern
8
+ # custom token replacements
9
+ attr_accessor :token_val_defaults
10
+ # the base regexp pattern used to recognize templated products.
11
+ attr_accessor :recognizer_pattern
12
+
13
+ DEFAULT_PATTERN_STR = ":dest_dir/:handle_prefix/:handle/:date/:handle+:timestamp-:pid-:hostname.tsv"
14
+
15
+ def initialize pattern, token_val_defaults={}
16
+ self.pattern = pattern
17
+ self.token_val_defaults = token_val_defaults
18
+ end
19
+
20
+ #
21
+ # walk through pattern, replacing tokens (eg :time or :pid) with the
22
+ # corresponding value.
23
+ #
24
+ def make token_vals={}
25
+ token_vals = token_val_defaults.merge token_vals
26
+ token_vals[:timestamp] ||= Time.now.utc.strftime("%Y%m%d%H%M%S")
27
+ val = pattern.gsub(/:(\w+)/){ replace($1, token_vals) }
28
+ val
29
+ end
30
+
31
+ def to_s token_vals={}
32
+ make token_vals
33
+ end
34
+
35
+ #
36
+ # walk through pattern, constructing a regexp for parsing a templated
37
+ # product of that pattern
38
+ #
39
+ # It's undefined if a token is repeated inconsistently, eg recognizing
40
+ # '20090426012345/foo-20070707070707' with ':timestamp/:handle-:timestamp'
41
+ # It's harmless to have a token that is repeated, but always identically.
42
+ #
43
+ def make_recognizer token_regexps={}
44
+ return RECOGNIZER_REGEXPS[pattern] if RECOGNIZER_REGEXPS[pattern]
45
+ tokens = []
46
+ recognizer = recognizer_pattern.gsub(/:(\\\{\w+\\\}|\w+)/) do
47
+ tok = $1.gsub(/\W/,'')
48
+ tokens << tok.to_sym
49
+ '('+recognize_replace(tok, token_regexps)+')'
50
+ end
51
+ RECOGNIZER_REGEXPS[pattern] = [Regexp.new(recognizer), tokens]
52
+ end
53
+
54
+ #
55
+ # substitute for token
56
+ #
57
+ def replace token, token_vals
58
+ token = token.to_sym
59
+ return token_vals[token] if token_vals.include? token
60
+ case token
61
+ when :pid then pid
62
+ when :hostname then hostname
63
+ when :handle then token_vals[:handle] || Monkeyshines::CONFIG[:handle]
64
+ when :handle_prefix then token_vals[:handle].to_s[0..5]
65
+ when :timestamp then token_vals[:timestamp]
66
+ when :date then token_vals[:timestamp][ 0..7]
67
+ when :time then token_vals[:timestamp][ 8..13]
68
+ when :hour then token_vals[:timestamp][ 8..9]
69
+ when :min then token_vals[:timestamp][10..11]
70
+ when :sec then token_vals[:timestamp][12..13]
71
+ else
72
+ raise "Don't know how to encode token #{token} #{token_vals[token]}"
73
+ end
74
+ end
75
+
76
+ # Memoized: the hostname for the machine running this script.
77
+ def hostname
78
+ @hostname ||= ENV['HOSTNAME'] || `hostname`
79
+ end
80
+ # Memoized: the Process ID for this invocation.
81
+ def pid
82
+ @pid ||= Process.pid
83
+ end
84
+
85
+ # Characters deemed safe in a filename;
86
+ SAFE_CHARS = 'a-zA-Z0-9_\-\.\+\/\;'
87
+ def self.sanitize str
88
+ str.gsub(%r{[^#{SAFE_CHARS}]+}, '-')
89
+ end
90
+
91
+ # The base regexp pattern used to recognize templated products.
92
+ # By default, it's just the regexp-escaped version of this pattern,
93
+ # rooted on the right hand side (the end of the pattern matches the end of
94
+ # the string)
95
+ def recognizer_pattern
96
+ @recognizer_pattern ||= Regexp.escape(pattern)+'\z'
97
+ end
98
+
99
+ def recognize_replace token, token_vals
100
+ token = token.to_sym
101
+ return token_vals[token] if token_vals.include? token
102
+ case token
103
+ when :pid then '\d{1,5}'
104
+ when :hostname then '[a-zA-Z][a-zA-Z0-9\-\.]*[a-zA-Z0-9]'
105
+ when :handle then '[\w\-\.]+'
106
+ when :handle_prefix then '[\w\-\.]+'
107
+ when :timestamp then '\d{14}'
108
+ when :date then '\d{8}'
109
+ when :time then '\d{6}'
110
+ when :hour then '\d{2}'
111
+ when :min then '\d{2}'
112
+ when :sec then '\d{2}'
113
+ when :ext then '[a-zA-Z0-9\.]+'
114
+ when :any_id then '\w+'
115
+ else
116
+ raise "Don't know how to encode token #{token} #{token_vals[token]}"
117
+ end
118
+ end
119
+
120
+ def recognize str, token_regexps={}
121
+ recognizer, tokens = make_recognizer token_regexps
122
+ unless m = recognizer.match(str)
123
+ warn "Can't match #{recognizer} against #{str}"
124
+ return
125
+ end
126
+ Hash.zip(tokens, m.captures)
127
+ end
128
+
129
+ def unrecognize
130
+ RECOGNIZER_REGEXPS.delete pattern
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,15 @@
1
+ module Monkeyshines
2
+ # Common logger
3
+ def self.logger
4
+ @logger ||= Monkeyshines.default_logger
5
+ end
6
+ def self.logger= logger
7
+ @logger = logger
8
+ end
9
+
10
+ def self.default_logger dest=nil
11
+ require 'logger'
12
+ dest ||= $stderr
13
+ Logger.new dest
14
+ end
15
+ end
@@ -0,0 +1,84 @@
1
+ Trollop FAQ
2
+ -----------
3
+
4
+ Q: Why is it called "Trollop"?
5
+ A: No reason.
6
+
7
+ Q: Why should I use Trollop?
8
+ A: Because it will take you FEWER LINES OF CODE to do reasonable option parsing
9
+ than any other option parser out there.
10
+
11
+ Look at this:
12
+
13
+ opts = Trollop::options do
14
+ opt :monkey, "Use monkey mode"
15
+ opt :goat, "Use goat mode", :default => true
16
+ opt :num_limbs, "Set number of limbs", :default => 4
17
+ end
18
+
19
+ That's it. And opts is a hash and you do whatever you want with it.
20
+ Trivial. You don't have to mix option processing code blocks with the
21
+ declarations. You don't have to make a class for every option (what is this,
22
+ Java?). You don't have to write more than 1 line of code per option.
23
+
24
+ Plus, you get a beautiful help screen that detects your terminal width and
25
+ wraps appropriately. C'mon, that's hot.
26
+
27
+ Q: What is the philosophy behind Trollop?
28
+ A: Must a commandline option processor have a philosophy?
29
+
30
+ Q: Seriously now. What is it?
31
+ A: Ok, it's this: Trollop *just* does the parsing and gives you a hash table
32
+ of the result. So whatever fancy logic or constraints you need, you can
33
+ implement by operating on that hash table. Options that disable other
34
+ options, fancy constraints involving multiple sets of values across multiple
35
+ sets of options, etc. are all left for you to do manually.
36
+
37
+ (Trollop does support limited constraint setting (see #conflicts and
38
+ #depends), but any non-trivial program will need to get fancier.)
39
+
40
+ The result is that using Trollop is pretty simple, and whatever bizarre
41
+ logic you want, you can write yourself. And don't forget, you can call
42
+ Trollop::die to abort the program and give a fancy options-related error
43
+ message.
44
+
45
+ Q: What happens to the other stuff on the commandline?
46
+ A: Anything Trollop doesn't recognize as an option or as an option parameter is
47
+ left in ARGV for you to process.
48
+
49
+ Q: Does Trollop support multiple-value arguments?
50
+ A: Yes. If you set the :type of an option to something plural, like ":ints",
51
+ ":strings", ":doubles", ":floats", ":ios", it will accept multiple arguments
52
+ on the commandline and the value will be an array of these.
53
+
54
+ Q: Does Trollop support arguments that can be given multiple times?
55
+ A: Yes. If you set :multi to true, then the argument can appear multiple times
56
+ on the commandline, and the value will be an array of the parameters.
57
+
58
+ Q: Does Trollop support subcommands?
59
+ A: Yes. You get subcommand support by adding a call #stop_on within the options
60
+ block, and passing the names of the subcommands to it. (See the third
61
+ example on the webpage.) When Trollop encounters one of these subcommands on
62
+ the commandline, it stops processing and returns.
63
+
64
+ ARGV at that point will contain the subcommand followed by any subcommand
65
+ options, since Trollop has contained the rest. So you can consume the
66
+ subcommand and call Trollop.options again with the particular options set
67
+ for that subcommand.
68
+
69
+ If you don't know the subcommands ahead of time, you can call
70
+ #stop_on_unknown, which will cause Trollop to stop when it encounters any
71
+ unknown token. This might be more trouble than its worth if you're also
72
+ passing filenames on the commandline.
73
+
74
+ It's probably easier to see the example on the webpage than to read all
75
+ that.
76
+
77
+ Q: Why does Trollop disallow numeric short argument names, like '-1' and '-9'?
78
+ A: Because it's ambiguous whether these are arguments or negative integer or
79
+ floating-point parameters to arguments. E.g., what about "-f -3". Is that a
80
+ negative three parameter to -f, or two separate parameters?
81
+
82
+ I could be very clever and detect when there are no arguments that require
83
+ floating-point parameters, and allow such short option names in those cases,
84
+ but opted for simplicity and consistency.
@@ -0,0 +1,101 @@
1
+ == 1.14 / 2009-06-19
2
+ * Make :multi arguments default to [], not nil, when not set on the commandline.
3
+ * Minor commenting and error message improvements
4
+
5
+ == 1.13 / 2009-03-16
6
+ * Fix parsing of "--longarg=<value with spaces>".
7
+
8
+ == 1.12 / 2009-01-30
9
+ * Fix some unit test failures in the last release. Should be more careful.
10
+ * Make default short options only be assigned *after* all user-specified
11
+ short options. Now there's a little less juggling to do when you just
12
+ want to specify a few short options.
13
+
14
+ == 1.11 / 2009-01-29
15
+ * Set <opt>_given keys in the results hash for options that were specified
16
+ on the commandline.
17
+
18
+ == 1.10.2 / 2008-10-23
19
+ * No longer try `stty size` for screen size detection. Just use curses, and
20
+ screen users will have to deal with the screen clearing.
21
+
22
+ == 1.10.1 / 2008-10-22
23
+ * Options hash now responds to method calls as well as standard hash lookup.
24
+ * Default values for multi-occurrence parameters now autoboxed.
25
+ * The relationship between multi-value, multi-occurrence, and default values
26
+ improved and explained.
27
+ * Documentation improvements.
28
+
29
+ == 1.10 / 2008-10-21
30
+ * Added :io type for parameters that point to IO streams (filenames, URIs, etc).
31
+ * For screen size detection, first try `stty size` before loading Curses.
32
+ * Improved documentation.
33
+
34
+ == 1.9 / 2008-08-20
35
+ * Added 'stop_on_unknown' command to stop parsing on any unknown argument.
36
+ This is useful for handling sub-commands when you don't know the entire
37
+ set of commands up front. (E.g. if the initial arguments can change it.)
38
+ * Added a :multi option for parameters, signifying that they can be specified
39
+ multiple times.
40
+ * Added :ints, :strings, :doubles, and :floats option types, which can take
41
+ multiple arguments.
42
+
43
+ == 1.8.2 / 2008-06-25
44
+ * Bugfix for #conflicts and #depends error messages
45
+
46
+ == 1.8.1 / 2008-06-24
47
+ * Bugfix for short option autocreation
48
+ * More aggressive documentation
49
+
50
+ == 1.8 / 2008-06-16
51
+ * Sub-command support via Parser#stop_on
52
+
53
+ == 1.7.2 / 2008-01-16
54
+ * Ruby 1.9-ify. Apparently this means replacing :'s with ;'s.
55
+
56
+ == 1.7.1 / 2008-01-07
57
+ * Documentation improvements
58
+
59
+ == 1.7 / 2007-06-17
60
+ * Fix incorrect error message for multiple missing required arguments
61
+ (thanks to Neill Zero)
62
+
63
+ == 1.6 / 2007-04-01
64
+ * Don't attempt curses screen-width magic unless running on a terminal.
65
+
66
+ == 1.5 / 2007-03-31
67
+ * --help and --version do the right thing even if the rest of the
68
+ command line is incorrect.
69
+ * Added #conflicts and #depends to model dependencies and exclusivity
70
+ between arguments.
71
+ * Minor bugfixes.
72
+
73
+ == 1.4 / 2007-03-26
74
+ * Disable short options with :short => :none.
75
+ * Minor bugfixes and error message improvements.
76
+
77
+ == 1.3 / 2007-01-31
78
+ * Wrap at (screen width - 1) instead of screen width.
79
+ * User can override --help and --version.
80
+ * Bugfix in handling of -v and -h.
81
+ * More tests to confirm the above.
82
+
83
+ == 1.2 / 2007-01-31
84
+ * Minor documentation tweaks.
85
+ * Removed hoe dependency.
86
+
87
+ == 1.1 / 2007-01-30
88
+ * Trollop::options now passes any arguments as block arguments. Since
89
+ instance variables are not properly captured by the block, this
90
+ makes it slightly less noisy to pass them in as local variables.
91
+ (A real-life use for _why's cloaker!)
92
+ * Help display now preserves original argument order.
93
+ * Trollop::die now also has a single string form in case death is not
94
+ due to a single argument.
95
+ * Parser#text now an alias for Parser#banner, and can be called
96
+ multiple times, with the output being placed in the right position
97
+ in the help text.
98
+ * Slightly more indicative formatting for parameterized arguments.
99
+
100
+ == 1.0 / 2007-01-29
101
+ * Initial release.
@@ -0,0 +1,7 @@
1
+ FAQ.txt
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/trollop.rb
7
+ test/test_trollop.rb
@@ -0,0 +1,40 @@
1
+ == trollop
2
+
3
+ by William Morgan (wmorgan-trollop at the masanjin dot nets or http://cs.stanford.edu/~ruby)
4
+
5
+ Main page: http://trollop.rubyforge.org
6
+
7
+ Release announcements and comments: http://all-thing.net/search/label/trollop
8
+
9
+ Documentation quickstart: See Trollop::options (for some reason rdoc isn't
10
+ linking that; it's in the top right of the screen if you're browsing online)
11
+ and then Trollop::Parser#opt. Also see the examples at http://trollop.rubyforge.org/.
12
+
13
+ == DESCRIPTION
14
+
15
+ Trollop is a commandline option parser for Ruby that just gets out of your
16
+ way. One line of code per option is all you need to write. For that, you get
17
+ a nice automatically-generated help page, robust option parsing, command
18
+ subcompletion, and sensible defaults for everything you don't specify.
19
+
20
+ == FEATURES/PROBLEMS
21
+
22
+ - Dirt-simple usage.
23
+ - Sensible defaults. No tweaking necessary, much tweaking possible.
24
+ - Support for long options, short options, short option bundling,
25
+ and automatic type validation and conversion.
26
+ - Support for subcommands.
27
+ - Automatic help message generation, wrapped to current screen width.
28
+ - Lots of unit tests.
29
+
30
+ == REQUIREMENTS
31
+
32
+ * A burning desire to write less code.
33
+
34
+ == INSTALL
35
+
36
+ * gem install trollop
37
+
38
+ == LICENSE
39
+
40
+ Copyright (c) 2008 William Morgan. Trollop is distributed under the same terms as Ruby.
@@ -0,0 +1,36 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ $:.unshift "lib"
7
+ require 'trollop'
8
+
9
+ class Hoe
10
+ def extra_dev_deps; @extra_dev_deps.reject { |x| x[0] == "hoe" } end
11
+ end
12
+
13
+ Hoe.new('trollop', Trollop::VERSION) do |p|
14
+ p.rubyforge_name = 'trollop'
15
+ p.author = "William Morgan"
16
+ p.summary = "Trollop is a commandline option parser for Ruby that just gets out of your way. One line of code per option is all you need to write. For that, you get a nice automatically-generated help page, robust option parsing, command subcompletion, and sensible defaults for everything you don't specify."
17
+ p.description = p.paragraphs_of('README.txt', 4..5, 9..18).join("\n\n").gsub(/== SYNOPSIS/, "Synopsis")
18
+ p.url = "http://trollop.rubyforge.org"
19
+ p.changes = p.paragraphs_of('History.txt', 0..0).join("\n\n")
20
+ p.email = "wmorgan-trollop@masanjin.net"
21
+ end
22
+
23
+ WWW_FILES = FileList["www/*"] + %w(README.txt FAQ.txt)
24
+ task :upload_webpage => WWW_FILES do |t|
25
+ sh "rsync -Paz -essh #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/trollop/"
26
+ end
27
+
28
+ task :rdoc do |t|
29
+ sh "rdoc lib README.txt History.txt -m README.txt"
30
+ end
31
+
32
+ task :upload_docs => :rdoc do |t|
33
+ sh "rsync -az -essh doc/* wmorgan@rubyforge.org:/var/www/gforge-projects/trollop/trollop/"
34
+ end
35
+
36
+ # vim: syntax=ruby
@@ -0,0 +1,744 @@
1
+ ## lib/trollop.rb -- trollop command-line processing library
2
+ ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3
+ ## Copyright:: Copyright 2007 William Morgan
4
+ ## License:: GNU GPL version 2
5
+
6
+ require 'date'
7
+
8
+ module Trollop
9
+
10
+ VERSION = "1.14"
11
+
12
+ ## Thrown by Parser in the event of a commandline error. Not needed if
13
+ ## you're using the Trollop::options entry.
14
+ class CommandlineError < StandardError; end
15
+
16
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
17
+ ## automatically by Trollop#options.
18
+ class HelpNeeded < StandardError; end
19
+
20
+ ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
21
+ ## automatically by Trollop#options.
22
+ class VersionNeeded < StandardError; end
23
+
24
+ ## Regex for floating point numbers
25
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
26
+
27
+ ## Regex for parameters
28
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
29
+
30
+ ## The commandline parser. In typical usage, the methods in this class
31
+ ## will be handled internally by Trollop::options. In this case, only the
32
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
33
+ ## typically be called.
34
+ ##
35
+ ## If it's necessary to instantiate this class (for more complicated
36
+ ## argument-parsing situations), be sure to call #parse to actually
37
+ ## produce the output hash.
38
+ class Parser
39
+
40
+ ## The set of values that indicate a flag option when passed as the
41
+ ## +:type+ parameter of #opt.
42
+ FLAG_TYPES = [:flag, :bool, :boolean]
43
+
44
+ ## The set of values that indicate a single-parameter (normal) option when
45
+ ## passed as the +:type+ parameter of #opt.
46
+ ##
47
+ ## A value of +io+ corresponds to a readable IO resource, including
48
+ ## a filename, URI, or the strings 'stdin' or '-'.
49
+ SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
50
+
51
+ ## The set of values that indicate a multiple-parameter option (i.e., that
52
+ ## takes multiple space-separated values on the commandline) when passed as
53
+ ## the +:type+ parameter of #opt.
54
+ MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
55
+
56
+ ## The complete set of legal values for the +:type+ parameter of #opt.
57
+ TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
58
+
59
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
60
+
61
+ ## The values from the commandline that were not interpreted by #parse.
62
+ attr_reader :leftovers
63
+
64
+ ## The complete configuration hashes for each option. (Mainly useful
65
+ ## for testing.)
66
+ attr_reader :specs
67
+
68
+ ## Initializes the parser, and instance-evaluates any block given.
69
+ def initialize *a, &b
70
+ @version = nil
71
+ @leftovers = []
72
+ @specs = {}
73
+ @long = {}
74
+ @short = {}
75
+ @order = []
76
+ @constraints = []
77
+ @stop_words = []
78
+ @stop_on_unknown = false
79
+
80
+ #instance_eval(&b) if b # can't take arguments
81
+ cloaker(&b).bind(self).call(*a) if b
82
+ end
83
+
84
+ ## Define an option. +name+ is the option name, a unique identifier
85
+ ## for the option that you will use internally, which should be a
86
+ ## symbol or a string. +desc+ is a string description which will be
87
+ ## displayed in help messages.
88
+ ##
89
+ ## Takes the following optional arguments:
90
+ ##
91
+ ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
92
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
93
+ ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
94
+ ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+.
95
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
96
+ ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)
97
+ ##
98
+ ## Note that there are two types of argument multiplicity: an argument
99
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
100
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
101
+ ##
102
+ ## Arguments that take multiple values should have a +:type+ parameter
103
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
104
+ ## value of an array of the correct type (e.g. [String]). The
105
+ ## value of this argument will be an array of the parameters on the
106
+ ## commandline.
107
+ ##
108
+ ## Arguments that can occur multiple times should be marked with
109
+ ## +:multi+ => +true+. The value of this argument will also be an array.
110
+ ## In contrast with regular non-multi options, if not specified on
111
+ ## the commandline, the default value will be [], not nil.
112
+ ##
113
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
114
+ ## +:multi+ => +true+), in which case the value of the argument will be
115
+ ## an array of arrays.
116
+ ##
117
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
118
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
119
+ ## is a multi-value argument as well as a multi-occurrence argument.
120
+ ## In thise case, Trollop assumes that it's not a multi-value argument.
121
+ ## If you want a multi-value, multi-occurrence argument with a default
122
+ ## value, you must specify +:type+ as well.
123
+
124
+ def opt name, desc="", opts={}
125
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
126
+
127
+ ## fill in :type
128
+ opts[:type] = # normalize
129
+ case opts[:type]
130
+ when :boolean, :bool; :flag
131
+ when :integer; :int
132
+ when :integers; :ints
133
+ when :double; :float
134
+ when :doubles; :floats
135
+ when Class
136
+ case opts[:type].name
137
+ when 'TrueClass', 'FalseClass'; :flag
138
+ when 'String'; :string
139
+ when 'Integer'; :int
140
+ when 'Float'; :float
141
+ when 'IO'; :io
142
+ when 'Date'; :date
143
+ else
144
+ raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
145
+ end
146
+ when nil; nil
147
+ else
148
+ raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
149
+ opts[:type]
150
+ end
151
+
152
+ ## for options with :multi => true, an array default doesn't imply
153
+ ## a multi-valued argument. for that you have to specify a :type
154
+ ## as well. (this is how we disambiguate an ambiguous situation;
155
+ ## see the docs for Parser#opt for details.)
156
+ disambiguated_default =
157
+ if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
158
+ opts[:default].first
159
+ else
160
+ opts[:default]
161
+ end
162
+
163
+ type_from_default =
164
+ case disambiguated_default
165
+ when Integer; :int
166
+ when Numeric; :float
167
+ when TrueClass, FalseClass; :flag
168
+ when String; :string
169
+ when IO; :io
170
+ when Date; :date
171
+ when Array
172
+ if opts[:default].empty?
173
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
174
+ end
175
+ case opts[:default][0] # the first element determines the types
176
+ when Integer; :ints
177
+ when Numeric; :floats
178
+ when String; :strings
179
+ when IO; :ios
180
+ when Date; :dates
181
+ else
182
+ raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
183
+ end
184
+ when nil; nil
185
+ else
186
+ raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
187
+ end
188
+
189
+ raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
190
+
191
+ opts[:type] = opts[:type] || type_from_default || :flag
192
+
193
+ ## fill in :long
194
+ opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
195
+ opts[:long] =
196
+ case opts[:long]
197
+ when /^--([^-].*)$/
198
+ $1
199
+ when /^[^-]/
200
+ opts[:long]
201
+ else
202
+ raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
203
+ end
204
+ raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
205
+
206
+ ## fill in :short
207
+ opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
208
+ opts[:short] = case opts[:short]
209
+ when /^-(.)$/; $1
210
+ when nil, :none, /^.$/; opts[:short]
211
+ else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
212
+ end
213
+
214
+ if opts[:short]
215
+ raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
216
+ raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
217
+ end
218
+
219
+ ## fill in :default for flags
220
+ opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
221
+
222
+ ## autobox :default for :multi (multi-occurrence) arguments
223
+ opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
224
+
225
+ ## fill in :multi
226
+ opts[:multi] ||= false
227
+
228
+ opts[:desc] ||= desc
229
+ @long[opts[:long]] = name
230
+ @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
231
+ @specs[name] = opts
232
+ @order << [:opt, name]
233
+ end
234
+
235
+ ## Sets the version string. If set, the user can request the version
236
+ ## on the commandline. Should probably be of the form "<program name>
237
+ ## <version number>".
238
+ def version s=nil; @version = s if s; @version end
239
+
240
+ ## Adds text to the help display. Can be interspersed with calls to
241
+ ## #opt to build a multi-section help page.
242
+ def banner s; @order << [:text, s] end
243
+ alias :text :banner
244
+
245
+ ## Marks two (or more!) options as requiring each other. Only handles
246
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
247
+ ## better modeled with Trollop::die.
248
+ def depends *syms
249
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
250
+ @constraints << [:depends, syms]
251
+ end
252
+
253
+ ## Marks two (or more!) options as conflicting.
254
+ def conflicts *syms
255
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
256
+ @constraints << [:conflicts, syms]
257
+ end
258
+
259
+ ## Defines a set of words which cause parsing to terminate when
260
+ ## encountered, such that any options to the left of the word are
261
+ ## parsed as usual, and options to the right of the word are left
262
+ ## intact.
263
+ ##
264
+ ## A typical use case would be for subcommand support, where these
265
+ ## would be set to the list of subcommands. A subsequent Trollop
266
+ ## invocation would then be used to parse subcommand options, after
267
+ ## shifting the subcommand off of ARGV.
268
+ def stop_on *words
269
+ @stop_words = [*words].flatten
270
+ end
271
+
272
+ ## Similar to #stop_on, but stops on any unknown word when encountered
273
+ ## (unless it is a parameter for an argument). This is useful for
274
+ ## cases where you don't know the set of subcommands ahead of time,
275
+ ## i.e., without first parsing the global options.
276
+ def stop_on_unknown
277
+ @stop_on_unknown = true
278
+ end
279
+
280
+ ## Parses the commandline. Typically called by Trollop::options.
281
+ def parse cmdline=ARGV
282
+ vals = {}
283
+ required = {}
284
+
285
+ opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
286
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
287
+
288
+ @specs.each do |sym, opts|
289
+ required[sym] = true if opts[:required]
290
+ vals[sym] = opts[:default]
291
+ vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
292
+ end
293
+
294
+ resolve_default_short_options
295
+
296
+ ## resolve symbols
297
+ given_args = {}
298
+ @leftovers = each_arg cmdline do |arg, params|
299
+ sym = case arg
300
+ when /^-([^-])$/
301
+ @short[$1]
302
+ when /^--([^-]\S*)$/
303
+ @long[$1]
304
+ else
305
+ raise CommandlineError, "invalid argument syntax: '#{arg}'"
306
+ end
307
+ raise CommandlineError, "unknown argument '#{arg}'" unless sym
308
+
309
+ if given_args.include?(sym) && !@specs[sym][:multi]
310
+ raise CommandlineError, "option '#{arg}' specified multiple times"
311
+ end
312
+
313
+ given_args[sym] ||= {}
314
+
315
+ given_args[sym][:arg] = arg
316
+ given_args[sym][:params] ||= []
317
+
318
+ # The block returns the number of parameters taken.
319
+ num_params_taken = 0
320
+
321
+ unless params.nil?
322
+ if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
323
+ given_args[sym][:params] << params[0, 1] # take the first parameter
324
+ num_params_taken = 1
325
+ elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
326
+ given_args[sym][:params] << params # take all the parameters
327
+ num_params_taken = params.size
328
+ end
329
+ end
330
+
331
+ num_params_taken
332
+ end
333
+
334
+ ## check for version and help args
335
+ raise VersionNeeded if given_args.include? :version
336
+ raise HelpNeeded if given_args.include? :help
337
+
338
+ ## check constraint satisfaction
339
+ @constraints.each do |type, syms|
340
+ constraint_sym = syms.find { |sym| given_args[sym] }
341
+ next unless constraint_sym
342
+
343
+ case type
344
+ when :depends
345
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
346
+ when :conflicts
347
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
348
+ end
349
+ end
350
+
351
+ required.each do |sym, val|
352
+ raise CommandlineError, "option '#{sym}' must be specified" unless given_args.include? sym
353
+ end
354
+
355
+ ## parse parameters
356
+ given_args.each do |sym, given_data|
357
+ arg = given_data[:arg]
358
+ params = given_data[:params]
359
+
360
+ opts = @specs[sym]
361
+ raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
362
+
363
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
364
+
365
+ case opts[:type]
366
+ when :flag
367
+ vals[sym] = !opts[:default]
368
+ when :int, :ints
369
+ vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
370
+ when :float, :floats
371
+ vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
372
+ when :string, :strings
373
+ vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
374
+ when :io, :ios
375
+ vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
376
+ when :date, :dates
377
+ vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
378
+ end
379
+
380
+ if SINGLE_ARG_TYPES.include?(opts[:type])
381
+ unless opts[:multi] # single parameter
382
+ vals[sym] = vals[sym][0][0]
383
+ else # multiple options, each with a single parameter
384
+ vals[sym] = vals[sym].map { |p| p[0] }
385
+ end
386
+ elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
387
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
388
+ end
389
+ # else: multiple options, with multiple parameters
390
+ end
391
+
392
+ ## allow openstruct-style accessors
393
+ class << vals
394
+ def method_missing(m, *args)
395
+ self[m] || self[m.to_s]
396
+ end
397
+ end
398
+ vals
399
+ end
400
+
401
+ def parse_date_parameter param, arg #:nodoc:
402
+ begin
403
+ begin
404
+ time = Chronic.parse(param)
405
+ rescue NameError
406
+ # chronic is not available
407
+ end
408
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
409
+ rescue ArgumentError => e
410
+ raise CommandlineError, "option '#{arg}' needs a date"
411
+ end
412
+ end
413
+
414
+ ## Print the help message to +stream+.
415
+ def educate stream=$stdout
416
+ width # just calculate it now; otherwise we have to be careful not to
417
+ # call this unless the cursor's at the beginning of a line.
418
+
419
+ left = {}
420
+ @specs.each do |name, spec|
421
+ left[name] = "--#{spec[:long]}" +
422
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
423
+ case spec[:type]
424
+ when :flag; ""
425
+ when :int; " <i>"
426
+ when :ints; " <i+>"
427
+ when :string; " <s>"
428
+ when :strings; " <s+>"
429
+ when :float; " <f>"
430
+ when :floats; " <f+>"
431
+ when :io; " <filename/uri>"
432
+ when :ios; " <filename/uri+>"
433
+ when :date; " <date>"
434
+ when :dates; " <date+>"
435
+ end
436
+ end
437
+
438
+ leftcol_width = left.values.map { |s| s.length }.max || 0
439
+ rightcol_start = leftcol_width + 6 # spaces
440
+
441
+ unless @order.size > 0 && @order.first.first == :text
442
+ stream.puts "#@version\n" if @version
443
+ stream.puts "Options:"
444
+ end
445
+
446
+ @order.each do |what, opt|
447
+ if what == :text
448
+ stream.puts wrap(opt)
449
+ next
450
+ end
451
+
452
+ spec = @specs[opt]
453
+ stream.printf " %#{leftcol_width}s: ", left[opt]
454
+ desc = spec[:desc] + begin
455
+ default_s = case spec[:default]
456
+ when $stdout; "<stdout>"
457
+ when $stdin; "<stdin>"
458
+ when $stderr; "<stderr>"
459
+ when Array
460
+ spec[:default].join(", ")
461
+ else
462
+ spec[:default].to_s
463
+ end
464
+
465
+ if spec[:default]
466
+ if spec[:desc] =~ /\.$/
467
+ " (Default: #{default_s})"
468
+ else
469
+ " (default: #{default_s})"
470
+ end
471
+ else
472
+ ""
473
+ end
474
+ end
475
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
476
+ end
477
+ end
478
+
479
+ def width #:nodoc:
480
+ @width ||= if $stdout.tty?
481
+ begin
482
+ require 'curses'
483
+ Curses::init_screen
484
+ x = Curses::cols
485
+ Curses::close_screen
486
+ x
487
+ rescue Exception
488
+ 80
489
+ end
490
+ else
491
+ 80
492
+ end
493
+ end
494
+
495
+ def wrap str, opts={} # :nodoc:
496
+ if str == ""
497
+ [""]
498
+ else
499
+ str.split("\n").map { |s| wrap_line s, opts }.flatten
500
+ end
501
+ end
502
+
503
+ private
504
+
505
+ ## yield successive arg, parameter pairs
506
+ def each_arg args
507
+ remains = []
508
+ i = 0
509
+
510
+ until i >= args.length
511
+ if @stop_words.member? args[i]
512
+ remains += args[i .. -1]
513
+ return remains
514
+ end
515
+ case args[i]
516
+ when /^--$/ # arg terminator
517
+ remains += args[(i + 1) .. -1]
518
+ return remains
519
+ when /^--(\S+?)=(.*)$/ # long argument with equals
520
+ yield "--#{$1}", [$2]
521
+ i += 1
522
+ when /^--(\S+)$/ # long argument
523
+ params = collect_argument_parameters(args, i + 1)
524
+ unless params.empty?
525
+ num_params_taken = yield args[i], params
526
+ unless num_params_taken
527
+ if @stop_on_unknown
528
+ remains += args[i + 1 .. -1]
529
+ return remains
530
+ else
531
+ remains += params
532
+ end
533
+ end
534
+ i += 1 + num_params_taken
535
+ else # long argument no parameter
536
+ yield args[i], nil
537
+ i += 1
538
+ end
539
+ when /^-(\S+)$/ # one or more short arguments
540
+ shortargs = $1.split(//)
541
+ shortargs.each_with_index do |a, j|
542
+ if j == (shortargs.length - 1)
543
+ params = collect_argument_parameters(args, i + 1)
544
+ unless params.empty?
545
+ num_params_taken = yield "-#{a}", params
546
+ unless num_params_taken
547
+ if @stop_on_unknown
548
+ remains += args[i + 1 .. -1]
549
+ return remains
550
+ else
551
+ remains += params
552
+ end
553
+ end
554
+ i += 1 + num_params_taken
555
+ else # argument no parameter
556
+ yield "-#{a}", nil
557
+ i += 1
558
+ end
559
+ else
560
+ yield "-#{a}", nil
561
+ end
562
+ end
563
+ else
564
+ if @stop_on_unknown
565
+ remains += args[i .. -1]
566
+ return remains
567
+ else
568
+ remains << args[i]
569
+ i += 1
570
+ end
571
+ end
572
+ end
573
+
574
+ remains
575
+ end
576
+
577
+ def parse_integer_parameter param, arg
578
+ raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
579
+ param.to_i
580
+ end
581
+
582
+ def parse_float_parameter param, arg
583
+ raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
584
+ param.to_f
585
+ end
586
+
587
+ def parse_io_parameter param, arg
588
+ case param
589
+ when /^(stdin|-)$/i; $stdin
590
+ else
591
+ require 'open-uri'
592
+ begin
593
+ open param
594
+ rescue SystemCallError => e
595
+ raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
596
+ end
597
+ end
598
+ end
599
+
600
+ def collect_argument_parameters args, start_at
601
+ params = []
602
+ pos = start_at
603
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
604
+ params << args[pos]
605
+ pos += 1
606
+ end
607
+ params
608
+ end
609
+
610
+ def resolve_default_short_options
611
+ @order.each do |type, name|
612
+ next unless type == :opt
613
+ opts = @specs[name]
614
+ next if opts[:short]
615
+
616
+ c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
617
+ raise ArgumentError, "can't generate a default short option name for #{opts[:long].inspect}: out of unique characters" unless c
618
+
619
+ opts[:short] = c
620
+ @short[c] = name
621
+ end
622
+ end
623
+
624
+ def wrap_line str, opts={}
625
+ prefix = opts[:prefix] || 0
626
+ width = opts[:width] || (self.width - 1)
627
+ start = 0
628
+ ret = []
629
+ until start > str.length
630
+ nextt =
631
+ if start + width >= str.length
632
+ str.length
633
+ else
634
+ x = str.rindex(/\s/, start + width)
635
+ x = str.index(/\s/, start) if x && x < start
636
+ x || str.length
637
+ end
638
+ ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
639
+ start = nextt + 1
640
+ end
641
+ ret
642
+ end
643
+
644
+ ## instance_eval but with ability to handle block arguments
645
+ ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
646
+ def cloaker &b
647
+ (class << self; self; end).class_eval do
648
+ define_method :cloaker_, &b
649
+ meth = instance_method :cloaker_
650
+ remove_method :cloaker_
651
+ meth
652
+ end
653
+ end
654
+ end
655
+
656
+ ## The top-level entry method into Trollop. Creates a Parser object,
657
+ ## passes the block to it, then parses +args+ with it, handling any
658
+ ## errors or requests for help or version information appropriately (and
659
+ ## then exiting). Modifies +args+ in place. Returns a hash of option
660
+ ## values.
661
+ ##
662
+ ## The block passed in should contain zero or more calls to +opt+
663
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
664
+ ## probably a call to +version+ (Parser#version).
665
+ ##
666
+ ## The returned block contains a value for every option specified with
667
+ ## +opt+. The value will be the value given on the commandline, or the
668
+ ## default value if the option was not specified on the commandline. For
669
+ ## every option specified on the commandline, a key "<option
670
+ ## name>_given" will also be set in the hash.
671
+ ##
672
+ ## Example:
673
+ ##
674
+ ## require 'trollop'
675
+ ## opts = Trollop::options do
676
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
677
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
678
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
679
+ ## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
680
+ ## end
681
+ ##
682
+ ## ## if called with no arguments
683
+ ## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
684
+ ##
685
+ ## ## if called with --monkey
686
+ ## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
687
+ ##
688
+ ## See more examples at http://trollop.rubyforge.org.
689
+ def options args = ARGV, *a, &b
690
+ @p = Parser.new(*a, &b)
691
+ Trollop.do_parse_args(@p)
692
+ end
693
+
694
+ def self.do_parse_args options_parser, args = ARGV
695
+ @p = options_parser
696
+ begin
697
+ vals = @p.parse args
698
+ args.clear
699
+ @p.leftovers.each { |l| args << l }
700
+ vals
701
+ rescue CommandlineError => e
702
+ $stderr.puts "Error: #{e.message}."
703
+ $stderr.puts "Try --help for help."
704
+ exit(-1)
705
+ rescue HelpNeeded
706
+ @p.educate
707
+ exit
708
+ rescue VersionNeeded
709
+ puts @p.version
710
+ exit
711
+ end
712
+ end
713
+
714
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
715
+ ## 'msg', and dies. Example:
716
+ ##
717
+ ## options do
718
+ ## opt :volume, :default => 0.0
719
+ ## end
720
+ ##
721
+ ## die :volume, "too loud" if opts[:volume] > 10.0
722
+ ## die :volume, "too soft" if opts[:volume] < 0.1
723
+ ##
724
+ ## In the one-argument case, simply print that message, a notice
725
+ ## about -h, and die. Example:
726
+ ##
727
+ ## options do
728
+ ## opt :whatever # ...
729
+ ## end
730
+ ##
731
+ ## Trollop::die "need at least one filename" if ARGV.empty?
732
+ def die arg, msg=nil
733
+ if msg
734
+ $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
735
+ else
736
+ $stderr.puts "Error: #{arg}."
737
+ end
738
+ $stderr.puts "Try --help for help."
739
+ exit(-1)
740
+ end
741
+
742
+ module_function :options, :die
743
+
744
+ end # module