geeklets 0.0.4

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.
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # =====================================================================
4
+ # = This script provided by Ted Wise. =
5
+ # = URL: http://tedwise.com/2009/07/08/new-geek-tool-weather-script/ =
6
+ # = Modified by me, to reference new gem locations, and to add =
7
+ # = Configuration interface. =
8
+ # =====================================================================
9
+
10
+ require 'rubygems'
11
+ require 'rexml/document'
12
+ require 'fileutils'
13
+ require 'cgi'
14
+ require 'net/http'
15
+
16
+ class Weather
17
+ DATA_URL = "http://apple.accuweather.com/adcbin/apple/Apple_Weather_Data.asp?zipcode"
18
+ LOOKUP_URL = "http://apple.accuweather.com/adcbin/apple/Apple_find_city.asp?location"
19
+ TEMP_FILE_DIR = "/tmp"
20
+ TEMP_FILE_PREFIX = "acweather"
21
+ TEMP_FILE_SUFFIX = "tmp"
22
+ LOCK_FILE_SUFFIX = "lck"
23
+ ICON_FILE_SUFFIX = "gif"
24
+ SHARED_ICON = "#{TEMP_FILE_DIR}/acweather-icon.#{ICON_FILE_SUFFIX}"
25
+
26
+ def intialize
27
+ @icon_location = "."
28
+ end
29
+
30
+ def temp_file_name(location)
31
+ "#{TEMP_FILE_DIR}/#{TEMP_FILE_PREFIX}-#{CGI.escape(location)}.#{TEMP_FILE_SUFFIX}"
32
+ end
33
+
34
+ def lock_file_name(location)
35
+ "#{TEMP_FILE_DIR}/#{TEMP_FILE_PREFIX}-#{CGI.escape(location)}.#{LOCK_FILE_SUFFIX}"
36
+ end
37
+
38
+ def temp_icon_name(location)
39
+ "#{TEMP_FILE_DIR}/acweather-icon-#{CGI.escape(opts[:zipcode])}.#{ICON_FILE_SUFFIX}"
40
+ end
41
+
42
+ def titleize(text)
43
+ small_words = %w(a an and as at but by en for if in of on or the to via vs. vs v v.)
44
+ special_characters = %w(-^)
45
+
46
+ string = text.split
47
+ string.map! do |word|
48
+ word.strip!
49
+
50
+ if [string.first, string.last].include?(word) then word.capitalize! end
51
+
52
+ next(word) if small_words.include?(word)
53
+ next(word) if special_characters.include?(word)
54
+ next(word) if word =~ /[A-Z]/
55
+
56
+ word = begin
57
+ unless (match = word.match(/[#{special_characters.to_s}]/))
58
+ word.sub(/\w/) { |letter| letter.upcase }
59
+ else
60
+ word.split(match.to_s).map! {|word| word.capitalize }.join(match.to_s)
61
+ end
62
+ end
63
+ end
64
+
65
+ string.join(' ')
66
+ end
67
+
68
+ def pull_weather(location, max_age)
69
+ # Check if another process is downloading the weather and block until it's done
70
+ while File.file?(lock_file_name(location))
71
+ sleep(0.1)
72
+ end
73
+
74
+ # Download the weather if it's out of date
75
+ if !File.file?(temp_file_name(location)) || ((Time.now - File.mtime(temp_file_name(location))) > max_age)
76
+ # Create the lock file
77
+ FileUtils.touch(lock_file_name(location))
78
+
79
+ `curl --silent -m 30 "#{DATA_URL}=#{CGI.escape(location)}" > #{temp_file_name(location)}`
80
+ if File.size(temp_file_name(location)) == 0
81
+ FileUtils.rm(temp_file_name(location))
82
+ end
83
+
84
+ # Remove the lock file
85
+ FileUtils.rm(lock_file_name(location))
86
+ end
87
+ end
88
+
89
+ def lookup_postal(name)
90
+ puts "Location = Postal code/Zipcode"
91
+ xml_data = Net::HTTP.get_response(URI.parse("#{LOOKUP_URL}=#{CGI.escape(name)}")).body
92
+
93
+ # extract event information
94
+ doc = REXML::Document.new(xml_data)
95
+
96
+ doc.elements.each('adc_Database/CityList/location') do |ele|
97
+ puts "#{ele.attributes['city']}, #{ele.attributes['state']} = #{ele.attributes['postal']}"
98
+ end
99
+ end
100
+
101
+
102
+ def parse_weather(location)
103
+ weather = {}
104
+
105
+ doc = REXML::Document.new File.new(temp_file_name(location))
106
+
107
+ time = (doc.elements.collect('adc_Database/CurrentConditions/Time') { |el| el.text.strip })[0]
108
+ hour = time[0...2].to_i
109
+ min = time[3..5]
110
+ if hour < 12
111
+ ampm = "AM"
112
+ else
113
+ ampm = "PM"
114
+ end
115
+ if hour == 0
116
+ hour = 12
117
+ end
118
+ if hour > 12
119
+ hour = hour - 12
120
+ end
121
+ weather["time"] = "#{hour}:#{min} #{ampm}"
122
+ weather["temperature"] = (doc.elements.collect('adc_Database/CurrentConditions/Temperature') { |el| el.text.strip })[0]
123
+ weather["realfeel"] = (doc.elements.collect('adc_Database/CurrentConditions/RealFeel') { |el| el.text.strip })[0]
124
+ weather["pressure"] = (doc.elements.collect('adc_Database/CurrentConditions/Pressure') { |el| el.text.strip })[0]
125
+ weather["humidity"] = ((doc.elements.collect('adc_Database/CurrentConditions/Humidity') { |el| el.text.strip })[0])[0...-1].to_i
126
+ icon = (doc.elements.collect('adc_Database/CurrentConditions/WeatherIcon') { |el| el.text.strip })[0]
127
+ weather["current_state"] = titleize((doc.elements.collect('adc_Database/CurrentConditions/WeatherText') { |el| el.text.strip })[0])
128
+ weather["wind_speed"] = (doc.elements.collect('adc_Database/CurrentConditions/WindSpeed') { |el| el.text.strip })[0]
129
+ weather["wind_direction"] = (doc.elements.collect('adc_Database/CurrentConditions/WindDirection') { |el| el.text.strip })[0]
130
+ weather["forecast"] = (doc.elements.collect('adc_Database/Forecast/day/TXT_Long') { |el| el.text.strip })[0]
131
+ weather["high_temperature"] = (doc.elements.collect('adc_Database/Forecast/day/High_Temperature') { |el| el.text.strip })[0]
132
+ weather["low_temperature"] = (doc.elements.collect('adc_Database/Forecast/day/Low_Temperature') { |el| el.text.strip })[0]
133
+ weather["icon"] = icon
134
+ if icon.size != 0
135
+ weather["icon_file"] = "#{@icon_location}/#{icon}.#{ICON_FILE_SUFFIX}"
136
+ end
137
+
138
+ weather
139
+ end
140
+
141
+ def run(params)
142
+
143
+ opts = Trollop::options(params) do
144
+ opt :zipcode, "Zipcode to retrieve weather for", :type => String
145
+ opt :summary, "Short summary of current conditions"
146
+ opt :forecast, "Long-term weather forecast"
147
+ opt :humidity, "Current humidity"
148
+ opt :longtemperature, "Current temperature in long form"
149
+ opt :temperature, "Current temperature"
150
+ opt :realfeel, "Current RealFeel temperature"
151
+ opt :current, "Current conditions"
152
+ opt :icon, "Weather icon"
153
+ opt :iconlocation, "Directory containing weather icons", :type => String
154
+ opt :date, "Date the weather was created"
155
+ opt :lookup, "Lookup a city postal code", :type => String
156
+ end
157
+
158
+ Trollop::die :zipcode, "must have a value" if opts[:zipcode] && opts[:zipcode].length == 0
159
+ Trollop::die :lookup, "must have a value" if opts[:lookup] && opts[:lookup].length == 0
160
+
161
+ if opts[:zipcode] && opts[:lookup]
162
+ puts "You must choose either --zipcode or --lookup, not both"
163
+ exit
164
+ end
165
+
166
+ if opts[:lookup]
167
+ lookup_postal(opts[:lookup])
168
+ exit
169
+ end
170
+
171
+ if opts[:iconlocation]
172
+ @icon_location = opts[:iconlocation]
173
+ end
174
+
175
+ pull_weather(opts[:zipcode], 1800)
176
+ weather = parse_weather(opts[:zipcode])
177
+
178
+ if weather['icon_file'].nil?
179
+ FileUtils.rm(SHARED_ICON) if File.file?(SHARED_ICON)
180
+ else
181
+ begin
182
+ FileUtils.cp(weather['icon_file'], SHARED_ICON)
183
+ FileUtils.cp(weather['icon_file'], "/tmp/acweather-icon-#{CGI.escape(opts[:zipcode])}.#{ICON_FILE_SUFFIX}")
184
+ rescue
185
+ warn "Unable to read from icon file '#{weather['icon_file']}' in directory '#{@icon_location}'. Use the --iconlocation argument to provide the appropriate directory."
186
+ end
187
+ end
188
+
189
+ if opts[:summary]
190
+ puts "#{weather['temperature']}F, #{weather['current_state']}"
191
+ end
192
+ if opts[:current]
193
+ puts "#{weather['current_state']} #{weather['high_temperature']}/#{weather['low_temperature']} - #{weather['forecast']}"
194
+ end
195
+ if opts[:longtemperature]
196
+ puts "#{weather['temperature']}F (#{weather['realfeel']}F)"
197
+ end
198
+ if opts[:temperature]
199
+ puts "#{weather['temperature']}"
200
+ end
201
+ if opts[:realfeel]
202
+ puts "#{weather['realfeel']}"
203
+ end
204
+ if opts[:humidity]
205
+ puts weather['humidity']
206
+ end
207
+ if opts[:date]
208
+ puts weather['time']
209
+ end
210
+ if opts[:icon]
211
+ puts weather['icon_file']
212
+ end
213
+ if opts[:forecast]
214
+ puts weather['forecast']
215
+ end
216
+ end
217
+
218
+ end
@@ -0,0 +1,51 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'pathname'
5
+ require 'utility'
6
+ require 'trollop'
7
+
8
+ class Geeklets
9
+
10
+ def self.show_usage
11
+ puts "Usage: geeklets <geeklet-script> [relevant-parameters-for-script]"
12
+ puts
13
+ end
14
+
15
+ def self.show_known_scripts
16
+ puts "These are the currently known geeklet scripts:"
17
+ puts
18
+ script_inventory.each { |script| puts "\t#{script}"}
19
+ puts
20
+ end
21
+
22
+ def self.script_inventory
23
+ cwd = File.dirname(__FILE__)
24
+ children = Pathname.new(cwd).children
25
+ children.reject! { |child| !child.directory? }
26
+ children.map! { |child| child.basename.to_s }
27
+ end
28
+
29
+ def self.run_geeklet(geeklet, params)
30
+ require "#{geeklet}/#{geeklet.downcase}"
31
+ obj = eval("#{geeklet}.new")
32
+ obj.run(params)
33
+ end
34
+
35
+ def self.run(params)
36
+ if params.empty?
37
+ show_usage
38
+ show_known_scripts
39
+ else
40
+ geeklet = params.shift
41
+ if script_inventory.include?(geeklet)
42
+ run_geeklet(geeklet, params)
43
+ else
44
+ puts "I do not know how to run the #{geeklet} geeklet."
45
+ show_known_scripts
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,29 @@
1
+ class Utility
2
+
3
+ def self.wrap_text(text, width, indent = 0, option = :all)
4
+ raise ArgumentError.new("Width must be at least 1 character wide.") if width <= 1
5
+ raise ArgumentError.new("Indent must be 0 for no indenting or a positive number.") if indent < 0
6
+ raise ArgumentError.new("Wrapping width must be greater than the indent amount.") if width <= indent
7
+ lines = []
8
+
9
+ curline = nil
10
+ indent_text = " " * indent
11
+ text.split.each do |word|
12
+ if (curline == nil)
13
+ # start a new line
14
+ curline = ""
15
+ curline << indent_text if ( (option == :all) || (option == :indent && lines.count == 0) || (option == :outdent && lines.count > 0) )
16
+ curline << word << " "
17
+ elsif (curline.length + word.length <= width)
18
+ curline << word << " "
19
+ else
20
+ lines << curline.chop
21
+ curline = nil
22
+ redo
23
+ end
24
+
25
+ end
26
+ lines << curline.chop if curline
27
+ lines.join("\n").chomp
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Geeklets" do
4
+
5
+ it "should know how to show usage"
6
+
7
+ it "should know how to show the valid scripts"
8
+
9
+ it "should know how to obtain the list of valid scripts"
10
+
11
+ it "should know how to run an individual script"
12
+
13
+ it "should know how to parse the parameters to determine what script to run"
14
+
15
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'geeklets'
6
+ require 'spec'
7
+ require 'spec/autorun'
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
@@ -0,0 +1,78 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Utility do
4
+
5
+ context "word wrapping" do
6
+
7
+ end
8
+
9
+ context "word wrapping without indents" do
10
+
11
+ it "should reject invalid wrapping width" do
12
+ invalid_widths = [-5, -1, 0, 1]
13
+ invalid_widths.each do |width|
14
+ lambda {Utility.wrap_text("Some Text", width)}.should raise_error(ArgumentError)
15
+ end
16
+ end
17
+
18
+ it "should wrap a long string appropriately" do
19
+ long_text = "This is a long string that will need to be wrapped onto multiple lines."
20
+ wrapped_text = Utility.wrap_text(long_text, 20)
21
+ wrapped_text.should == "This is a long\nstring that will\nneed to be wrapped\nonto multiple lines."
22
+ wrapped_text.split("\n").count.should == 4
23
+ wrapped_text.split("\n").all? {|line| line.length <= 20}.should be_true
24
+ end
25
+
26
+ it "should allow short strings to remain short" do
27
+ short_text = "This is a short string."
28
+ wrapped_text = Utility.wrap_text(short_text, 100)
29
+ wrapped_text.should == short_text
30
+ end
31
+
32
+
33
+ end
34
+
35
+ context "word wrapping with indents" do
36
+
37
+ before :each do
38
+ @long_text = "This is a long string that will need to be wrapped onto multiple lines."
39
+ end
40
+
41
+ it "should reject invalid indent values" do
42
+ invalid_indents = [-5, -1]
43
+ invalid_indents.each do |indent|
44
+ lambda {Utility.wrap_text(@long_text, 20, indent)}.should raise_error(ArgumentError)
45
+ end
46
+ end
47
+
48
+ it "should reject wrapping widths that are too small for the indent value" do
49
+ invalid_widths = [1, 6, 10]
50
+ invalid_widths.each do |width|
51
+ lambda {Utility.wrap_text(@long_text, width, 10)}.should raise_error(ArgumentError)
52
+ end
53
+ end
54
+
55
+ it "should allow indenting of all rows" do
56
+ wrapped_text = Utility.wrap_text(@long_text, 25, 5, :all)
57
+ wrapped_text.should == " This is a long\n string that will\n need to be wrapped\n onto multiple lines."
58
+ wrapped_text.split("\n").count.should == 4
59
+ wrapped_text.split("\n").all? {|line| line.length <= 25 && line[0..4] == " "}.should be_true
60
+ end
61
+
62
+ it "should allow indenting of the first row only" do
63
+ wrapped_text = Utility.wrap_text(@long_text, 25, 5, :indent)
64
+ wrapped_text.should == " This is a long\nstring that will need to\nbe wrapped onto multiple\nlines."
65
+ wrapped_text.split("\n").count.should == 4
66
+ wrapped_text.split("\n").all? {|line| line.length <= 25 }.should be_true
67
+ end
68
+
69
+ it "should allow indenting of all but the first row" do
70
+ wrapped_text = Utility.wrap_text(@long_text, 25, 5, :outdent)
71
+ wrapped_text.should == "This is a long string\n that will need to be\n wrapped onto\n multiple lines."
72
+ wrapped_text.split("\n").count.should == 4
73
+ wrapped_text.split("\n").all? {|line| line.length <= 25 }.should be_true
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,760 @@
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:: the same terms as ruby itself
5
+
6
+ require 'date'
7
+ require 'time'
8
+
9
+ module Trollop
10
+
11
+ VERSION = "1.15"
12
+
13
+ ## Thrown by Parser in the event of a commandline error. Not needed if
14
+ ## you're using the Trollop::options entry.
15
+ class CommandlineError < StandardError; end
16
+
17
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
18
+ ## automatically by Trollop#options.
19
+ class HelpNeeded < StandardError; end
20
+
21
+ ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
22
+ ## automatically by Trollop#options.
23
+ class VersionNeeded < StandardError; end
24
+
25
+ ## Regex for floating point numbers
26
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
27
+
28
+ ## Regex for parameters
29
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
30
+
31
+ ## The commandline parser. In typical usage, the methods in this class
32
+ ## will be handled internally by Trollop::options. In this case, only the
33
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
34
+ ## typically be called.
35
+ ##
36
+ ## If it's necessary to instantiate this class (for more complicated
37
+ ## argument-parsing situations), be sure to call #parse to actually
38
+ ## produce the output hash.
39
+ class Parser
40
+
41
+ ## The set of values that indicate a flag option when passed as the
42
+ ## +:type+ parameter of #opt.
43
+ FLAG_TYPES = [:flag, :bool, :boolean]
44
+
45
+ ## The set of values that indicate a single-parameter (normal) option when
46
+ ## passed as the +:type+ parameter of #opt.
47
+ ##
48
+ ## A value of +io+ corresponds to a readable IO resource, including
49
+ ## a filename, URI, or the strings 'stdin' or '-'.
50
+ SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date, :time]
51
+
52
+ ## The set of values that indicate a multiple-parameter option (i.e., that
53
+ ## takes multiple space-separated values on the commandline) when passed as
54
+ ## the +:type+ parameter of #opt.
55
+ MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates, :times]
56
+
57
+ ## The complete set of legal values for the +:type+ parameter of #opt.
58
+ TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
59
+
60
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
61
+
62
+ ## The values from the commandline that were not interpreted by #parse.
63
+ attr_reader :leftovers
64
+
65
+ ## The complete configuration hashes for each option. (Mainly useful
66
+ ## for testing.)
67
+ attr_reader :specs
68
+
69
+ ## Initializes the parser, and instance-evaluates any block given.
70
+ def initialize *a, &b
71
+ @version = nil
72
+ @leftovers = []
73
+ @specs = {}
74
+ @long = {}
75
+ @short = {}
76
+ @order = []
77
+ @constraints = []
78
+ @stop_words = []
79
+ @stop_on_unknown = false
80
+
81
+ #instance_eval(&b) if b # can't take arguments
82
+ cloaker(&b).bind(self).call(*a) if b
83
+ end
84
+
85
+ ## Define an option. +name+ is the option name, a unique identifier
86
+ ## for the option that you will use internally, which should be a
87
+ ## symbol or a string. +desc+ is a string description which will be
88
+ ## displayed in help messages.
89
+ ##
90
+ ## Takes the following optional arguments:
91
+ ##
92
+ ## [+: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.
93
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
94
+ ## [+: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.
95
+ ## [+: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+.
96
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
97
+ ## [+: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.)
98
+ ##
99
+ ## Note that there are two types of argument multiplicity: an argument
100
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
101
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
102
+ ##
103
+ ## Arguments that take multiple values should have a +:type+ parameter
104
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
105
+ ## value of an array of the correct type (e.g. [String]). The
106
+ ## value of this argument will be an array of the parameters on the
107
+ ## commandline.
108
+ ##
109
+ ## Arguments that can occur multiple times should be marked with
110
+ ## +:multi+ => +true+. The value of this argument will also be an array.
111
+ ## In contrast with regular non-multi options, if not specified on
112
+ ## the commandline, the default value will be [], not nil.
113
+ ##
114
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
115
+ ## +:multi+ => +true+), in which case the value of the argument will be
116
+ ## an array of arrays.
117
+ ##
118
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
119
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
120
+ ## is a multi-value argument as well as a multi-occurrence argument.
121
+ ## In thise case, Trollop assumes that it's not a multi-value argument.
122
+ ## If you want a multi-value, multi-occurrence argument with a default
123
+ ## value, you must specify +:type+ as well.
124
+
125
+ def opt name, desc="", opts={}
126
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
127
+
128
+ ## fill in :type
129
+ opts[:type] = # normalize
130
+ case opts[:type]
131
+ when :boolean, :bool; :flag
132
+ when :integer; :int
133
+ when :integers; :ints
134
+ when :double; :float
135
+ when :doubles; :floats
136
+ when Class
137
+ case opts[:type].name
138
+ when 'TrueClass', 'FalseClass'; :flag
139
+ when 'String'; :string
140
+ when 'Integer'; :int
141
+ when 'Float'; :float
142
+ when 'IO'; :io
143
+ when 'Date'; :date
144
+ when 'Time'; :time
145
+ else
146
+ raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
147
+ end
148
+ when nil; nil
149
+ else
150
+ raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
151
+ opts[:type]
152
+ end
153
+
154
+ ## for options with :multi => true, an array default doesn't imply
155
+ ## a multi-valued argument. for that you have to specify a :type
156
+ ## as well. (this is how we disambiguate an ambiguous situation;
157
+ ## see the docs for Parser#opt for details.)
158
+ disambiguated_default =
159
+ if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
160
+ opts[:default].first
161
+ else
162
+ opts[:default]
163
+ end
164
+
165
+ type_from_default =
166
+ case disambiguated_default
167
+ when Integer; :int
168
+ when Numeric; :float
169
+ when TrueClass, FalseClass; :flag
170
+ when String; :string
171
+ when IO; :io
172
+ when Date; :date
173
+ when Time; :time
174
+ when Array
175
+ if opts[:default].empty?
176
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
177
+ end
178
+ case opts[:default][0] # the first element determines the types
179
+ when Integer; :ints
180
+ when Numeric; :floats
181
+ when String; :strings
182
+ when IO; :ios
183
+ when Date; :dates
184
+ when Time; :times
185
+ else
186
+ raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
187
+ end
188
+ when nil; nil
189
+ else
190
+ raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
191
+ end
192
+
193
+ 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
194
+
195
+ opts[:type] = opts[:type] || type_from_default || :flag
196
+
197
+ ## fill in :long
198
+ opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
199
+ opts[:long] =
200
+ case opts[:long]
201
+ when /^--([^-].*)$/
202
+ $1
203
+ when /^[^-]/
204
+ opts[:long]
205
+ else
206
+ raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
207
+ end
208
+ raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
209
+
210
+ ## fill in :short
211
+ opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
212
+ opts[:short] = case opts[:short]
213
+ when /^-(.)$/; $1
214
+ when nil, :none, /^.$/; opts[:short]
215
+ else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
216
+ end
217
+
218
+ if opts[:short]
219
+ raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
220
+ raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
221
+ end
222
+
223
+ ## fill in :default for flags
224
+ opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
225
+
226
+ ## autobox :default for :multi (multi-occurrence) arguments
227
+ opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
228
+
229
+ ## fill in :multi
230
+ opts[:multi] ||= false
231
+
232
+ opts[:desc] ||= desc
233
+ @long[opts[:long]] = name
234
+ @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
235
+ @specs[name] = opts
236
+ @order << [:opt, name]
237
+ end
238
+
239
+ ## Sets the version string. If set, the user can request the version
240
+ ## on the commandline. Should probably be of the form "<program name>
241
+ ## <version number>".
242
+ def version s=nil; @version = s if s; @version end
243
+
244
+ ## Adds text to the help display. Can be interspersed with calls to
245
+ ## #opt to build a multi-section help page.
246
+ def banner s; @order << [:text, s] end
247
+ alias :text :banner
248
+
249
+ ## Marks two (or more!) options as requiring each other. Only handles
250
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
251
+ ## better modeled with Trollop::die.
252
+ def depends *syms
253
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
254
+ @constraints << [:depends, syms]
255
+ end
256
+
257
+ ## Marks two (or more!) options as conflicting.
258
+ def conflicts *syms
259
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
260
+ @constraints << [:conflicts, syms]
261
+ end
262
+
263
+ ## Defines a set of words which cause parsing to terminate when
264
+ ## encountered, such that any options to the left of the word are
265
+ ## parsed as usual, and options to the right of the word are left
266
+ ## intact.
267
+ ##
268
+ ## A typical use case would be for subcommand support, where these
269
+ ## would be set to the list of subcommands. A subsequent Trollop
270
+ ## invocation would then be used to parse subcommand options, after
271
+ ## shifting the subcommand off of ARGV.
272
+ def stop_on *words
273
+ @stop_words = [*words].flatten
274
+ end
275
+
276
+ ## Similar to #stop_on, but stops on any unknown word when encountered
277
+ ## (unless it is a parameter for an argument). This is useful for
278
+ ## cases where you don't know the set of subcommands ahead of time,
279
+ ## i.e., without first parsing the global options.
280
+ def stop_on_unknown
281
+ @stop_on_unknown = true
282
+ end
283
+
284
+ ## Parses the commandline. Typically called by Trollop::options.
285
+ def parse cmdline=ARGV
286
+ vals = {}
287
+ required = {}
288
+
289
+ opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
290
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
291
+
292
+ @specs.each do |sym, opts|
293
+ required[sym] = true if opts[:required]
294
+ vals[sym] = opts[:default]
295
+ vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
296
+ end
297
+
298
+ resolve_default_short_options
299
+
300
+ ## resolve symbols
301
+ given_args = {}
302
+ @leftovers = each_arg cmdline do |arg, params|
303
+ sym = case arg
304
+ when /^-([^-])$/
305
+ @short[$1]
306
+ when /^--([^-]\S*)$/
307
+ @long[$1]
308
+ else
309
+ raise CommandlineError, "invalid argument syntax: '#{arg}'"
310
+ end
311
+ raise CommandlineError, "unknown argument '#{arg}'" unless sym
312
+
313
+ if given_args.include?(sym) && !@specs[sym][:multi]
314
+ raise CommandlineError, "option '#{arg}' specified multiple times"
315
+ end
316
+
317
+ given_args[sym] ||= {}
318
+
319
+ given_args[sym][:arg] = arg
320
+ given_args[sym][:params] ||= []
321
+
322
+ # The block returns the number of parameters taken.
323
+ num_params_taken = 0
324
+
325
+ unless params.nil?
326
+ if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
327
+ given_args[sym][:params] << params[0, 1] # take the first parameter
328
+ num_params_taken = 1
329
+ elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
330
+ given_args[sym][:params] << params # take all the parameters
331
+ num_params_taken = params.size
332
+ end
333
+ end
334
+
335
+ num_params_taken
336
+ end
337
+
338
+ ## check for version and help args
339
+ raise VersionNeeded if given_args.include? :version
340
+ raise HelpNeeded if given_args.include? :help
341
+
342
+ ## check constraint satisfaction
343
+ @constraints.each do |type, syms|
344
+ constraint_sym = syms.find { |sym| given_args[sym] }
345
+ next unless constraint_sym
346
+
347
+ case type
348
+ when :depends
349
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
350
+ when :conflicts
351
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
352
+ end
353
+ end
354
+
355
+ required.each do |sym, val|
356
+ raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
357
+ end
358
+
359
+ ## parse parameters
360
+ given_args.each do |sym, given_data|
361
+ arg = given_data[:arg]
362
+ params = given_data[:params]
363
+
364
+ opts = @specs[sym]
365
+ raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
366
+
367
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
368
+
369
+ case opts[:type]
370
+ when :flag
371
+ vals[sym] = !opts[:default]
372
+ when :int, :ints
373
+ vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
374
+ when :float, :floats
375
+ vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
376
+ when :string, :strings
377
+ vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
378
+ when :io, :ios
379
+ vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
380
+ when :date, :dates
381
+ vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
382
+ when :time, :times
383
+ vals[sym] = params.map { |pg| pg.map { |p| parse_time_parameter p, arg } }
384
+ end
385
+
386
+ if SINGLE_ARG_TYPES.include?(opts[:type])
387
+ unless opts[:multi] # single parameter
388
+ vals[sym] = vals[sym][0][0]
389
+ else # multiple options, each with a single parameter
390
+ vals[sym] = vals[sym].map { |p| p[0] }
391
+ end
392
+ elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
393
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
394
+ end
395
+ # else: multiple options, with multiple parameters
396
+ end
397
+
398
+ ## allow openstruct-style accessors
399
+ class << vals
400
+ def method_missing(m, *args)
401
+ self[m] || self[m.to_s]
402
+ end
403
+ end
404
+ vals
405
+ end
406
+
407
+ def parse_date_parameter param, arg #:nodoc:
408
+ begin
409
+ begin
410
+ time = Chronic.parse(param)
411
+ rescue NameError
412
+ # chronic is not available
413
+ end
414
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
415
+ rescue ArgumentError => e
416
+ raise CommandlineError, "option '#{arg}' needs a date"
417
+ end
418
+ end
419
+
420
+ def parse_time_parameter param, arg #:nodoc:
421
+ begin
422
+ begin
423
+ time = Chronic.parse(param)
424
+ rescue NameError
425
+ # chronic is not available
426
+ end
427
+ time ? Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec) : Time.parse(param)
428
+ rescue ArgumentError => e
429
+ raise CommandlineError, "option '#{arg}' needs a date"
430
+ end
431
+ end
432
+
433
+ ## Print the help message to +stream+.
434
+ def educate stream=$stdout
435
+ width # just calculate it now; otherwise we have to be careful not to
436
+ # call this unless the cursor's at the beginning of a line.
437
+
438
+ left = {}
439
+ @specs.each do |name, spec|
440
+ left[name] = "--#{spec[:long]}" +
441
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
442
+ case spec[:type]
443
+ when :flag; ""
444
+ when :int; " <i>"
445
+ when :ints; " <i+>"
446
+ when :string; " <s>"
447
+ when :strings; " <s+>"
448
+ when :float; " <f>"
449
+ when :floats; " <f+>"
450
+ when :io; " <filename/uri>"
451
+ when :ios; " <filename/uri+>"
452
+ when :date; " <date>"
453
+ when :dates; " <date+>"
454
+ when :time; " <time>"
455
+ when :times; " <time+>"
456
+ end
457
+ end
458
+
459
+ leftcol_width = left.values.map { |s| s.length }.max || 0
460
+ rightcol_start = leftcol_width + 6 # spaces
461
+
462
+ unless @order.size > 0 && @order.first.first == :text
463
+ stream.puts "#@version\n" if @version
464
+ stream.puts "Options:"
465
+ end
466
+
467
+ @order.each do |what, opt|
468
+ if what == :text
469
+ stream.puts wrap(opt)
470
+ next
471
+ end
472
+
473
+ spec = @specs[opt]
474
+ stream.printf " %#{leftcol_width}s: ", left[opt]
475
+ desc = spec[:desc] + begin
476
+ default_s = case spec[:default]
477
+ when $stdout; "<stdout>"
478
+ when $stdin; "<stdin>"
479
+ when $stderr; "<stderr>"
480
+ when Array
481
+ spec[:default].join(", ")
482
+ else
483
+ spec[:default].to_s
484
+ end
485
+
486
+ if spec[:default]
487
+ if spec[:desc] =~ /\.$/
488
+ " (Default: #{default_s})"
489
+ else
490
+ " (default: #{default_s})"
491
+ end
492
+ else
493
+ ""
494
+ end
495
+ end
496
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
497
+ end
498
+ end
499
+
500
+ def width #:nodoc:
501
+ @width ||= if $stdout.tty?
502
+ begin
503
+ require 'curses'
504
+ Curses::init_screen
505
+ x = Curses::cols
506
+ Curses::close_screen
507
+ x
508
+ rescue Exception
509
+ 80
510
+ end
511
+ else
512
+ 80
513
+ end
514
+ end
515
+
516
+ def wrap str, opts={} # :nodoc:
517
+ if str == ""
518
+ [""]
519
+ else
520
+ str.split("\n").map { |s| wrap_line s, opts }.flatten
521
+ end
522
+ end
523
+
524
+ private
525
+
526
+ ## yield successive arg, parameter pairs
527
+ def each_arg args
528
+ remains = []
529
+ i = 0
530
+
531
+ until i >= args.length
532
+ if @stop_words.member? args[i]
533
+ remains += args[i .. -1]
534
+ return remains
535
+ end
536
+ case args[i]
537
+ when /^--$/ # arg terminator
538
+ remains += args[(i + 1) .. -1]
539
+ return remains
540
+ when /^--(\S+?)=(.*)$/ # long argument with equals
541
+ yield "--#{$1}", [$2]
542
+ i += 1
543
+ when /^--(\S+)$/ # long argument
544
+ params = collect_argument_parameters(args, i + 1)
545
+ unless params.empty?
546
+ num_params_taken = yield args[i], params
547
+ unless num_params_taken
548
+ if @stop_on_unknown
549
+ remains += args[i + 1 .. -1]
550
+ return remains
551
+ else
552
+ remains += params
553
+ end
554
+ end
555
+ i += 1 + num_params_taken
556
+ else # long argument no parameter
557
+ yield args[i], nil
558
+ i += 1
559
+ end
560
+ when /^-(\S+)$/ # one or more short arguments
561
+ shortargs = $1.split(//)
562
+ shortargs.each_with_index do |a, j|
563
+ if j == (shortargs.length - 1)
564
+ params = collect_argument_parameters(args, i + 1)
565
+ unless params.empty?
566
+ num_params_taken = yield "-#{a}", params
567
+ unless num_params_taken
568
+ if @stop_on_unknown
569
+ remains += args[i + 1 .. -1]
570
+ return remains
571
+ else
572
+ remains += params
573
+ end
574
+ end
575
+ i += 1 + num_params_taken
576
+ else # argument no parameter
577
+ yield "-#{a}", nil
578
+ i += 1
579
+ end
580
+ else
581
+ yield "-#{a}", nil
582
+ end
583
+ end
584
+ else
585
+ if @stop_on_unknown
586
+ remains += args[i .. -1]
587
+ return remains
588
+ else
589
+ remains << args[i]
590
+ i += 1
591
+ end
592
+ end
593
+ end
594
+
595
+ remains
596
+ end
597
+
598
+ def parse_integer_parameter param, arg
599
+ raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
600
+ param.to_i
601
+ end
602
+
603
+ def parse_float_parameter param, arg
604
+ raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
605
+ param.to_f
606
+ end
607
+
608
+ def parse_io_parameter param, arg
609
+ case param
610
+ when /^(stdin|-)$/i; $stdin
611
+ else
612
+ require 'open-uri'
613
+ begin
614
+ open param
615
+ rescue SystemCallError => e
616
+ raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
617
+ end
618
+ end
619
+ end
620
+
621
+ def collect_argument_parameters args, start_at
622
+ params = []
623
+ pos = start_at
624
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
625
+ params << args[pos]
626
+ pos += 1
627
+ end
628
+ params
629
+ end
630
+
631
+ def resolve_default_short_options
632
+ @order.each do |type, name|
633
+ next unless type == :opt
634
+ opts = @specs[name]
635
+ next if opts[:short]
636
+
637
+ c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
638
+ if c # found a character to use
639
+ opts[:short] = c
640
+ @short[c] = name
641
+ end
642
+ end
643
+ end
644
+
645
+ def wrap_line str, opts={}
646
+ prefix = opts[:prefix] || 0
647
+ width = opts[:width] || (self.width - 1)
648
+ start = 0
649
+ ret = []
650
+ until start > str.length
651
+ nextt =
652
+ if start + width >= str.length
653
+ str.length
654
+ else
655
+ x = str.rindex(/\s/, start + width)
656
+ x = str.index(/\s/, start) if x && x < start
657
+ x || str.length
658
+ end
659
+ ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
660
+ start = nextt + 1
661
+ end
662
+ ret
663
+ end
664
+
665
+ ## instance_eval but with ability to handle block arguments
666
+ ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
667
+ def cloaker &b
668
+ (class << self; self; end).class_eval do
669
+ define_method :cloaker_, &b
670
+ meth = instance_method :cloaker_
671
+ remove_method :cloaker_
672
+ meth
673
+ end
674
+ end
675
+ end
676
+
677
+ ## The top-level entry method into Trollop. Creates a Parser object,
678
+ ## passes the block to it, then parses +args+ with it, handling any
679
+ ## errors or requests for help or version information appropriately (and
680
+ ## then exiting). Modifies +args+ in place. Returns a hash of option
681
+ ## values.
682
+ ##
683
+ ## The block passed in should contain zero or more calls to +opt+
684
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
685
+ ## probably a call to +version+ (Parser#version).
686
+ ##
687
+ ## The returned block contains a value for every option specified with
688
+ ## +opt+. The value will be the value given on the commandline, or the
689
+ ## default value if the option was not specified on the commandline. For
690
+ ## every option specified on the commandline, a key "<option
691
+ ## name>_given" will also be set in the hash.
692
+ ##
693
+ ## Example:
694
+ ##
695
+ ## require 'trollop'
696
+ ## opts = Trollop::options do
697
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
698
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
699
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
700
+ ## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
701
+ ## end
702
+ ##
703
+ ## ## if called with no arguments
704
+ ## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
705
+ ##
706
+ ## ## if called with --monkey
707
+ ## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
708
+ ##
709
+ ## See more examples at http://trollop.rubyforge.org.
710
+ def options args = ARGV, *a, &b
711
+ @p = Parser.new(*a, &b)
712
+ begin
713
+ vals = @p.parse args
714
+ args.clear
715
+ @p.leftovers.each { |l| args << l }
716
+ vals
717
+ rescue CommandlineError => e
718
+ $stderr.puts "Error: #{e.message}."
719
+ $stderr.puts "Try --help for help."
720
+ exit(-1)
721
+ rescue HelpNeeded
722
+ @p.educate
723
+ exit
724
+ rescue VersionNeeded
725
+ puts @p.version
726
+ exit
727
+ end
728
+ end
729
+
730
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
731
+ ## 'msg', and dies. Example:
732
+ ##
733
+ ## options do
734
+ ## opt :volume, :default => 0.0
735
+ ## end
736
+ ##
737
+ ## die :volume, "too loud" if opts[:volume] > 10.0
738
+ ## die :volume, "too soft" if opts[:volume] < 0.1
739
+ ##
740
+ ## In the one-argument case, simply print that message, a notice
741
+ ## about -h, and die. Example:
742
+ ##
743
+ ## options do
744
+ ## opt :whatever # ...
745
+ ## end
746
+ ##
747
+ ## Trollop::die "need at least one filename" if ARGV.empty?
748
+ def die arg, msg=nil
749
+ if msg
750
+ $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
751
+ else
752
+ $stderr.puts "Error: #{arg}."
753
+ end
754
+ $stderr.puts "Try --help for help."
755
+ exit(-1)
756
+ end
757
+
758
+ module_function :options, :die
759
+
760
+ end # module