sdm 0.5

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.
data/README ADDED
@@ -0,0 +1,18 @@
1
+ Stack DB Migrator Helper
2
+ ========================
3
+
4
+ Wraps the mvn command for the [LDS Stack DB Migrator][1] to make it easier to use.
5
+
6
+ Commands
7
+ --------
8
+
9
+ envs Show available environments to run migrations on.
10
+ status st Display status of database. See pending migrations.
11
+ migrate mi Apply scripts in queue to bring database to target.
12
+ execute exec x Run specified script ad hoc. Without logging.
13
+ new Create a new blank script with timestamp in name.
14
+ drop Delete all objects in the database.
15
+
16
+ See `sdm <command> -h` to get additional help and usage on a specific command.
17
+
18
+ [1]: http://code.lds.org/maven-sites/stack/module.html?module=db-migrator
data/TODO ADDED
@@ -0,0 +1,9 @@
1
+ TODO
2
+ ====
3
+
4
+ - gemspec
5
+ - remove dependency on `pw` for passwords
6
+ maybe save an encrypted form of the password in the properties files
7
+ - talk to Bruce Campbell about the new task
8
+ have him not require a password and properties file
9
+ - talk to Bruce about drop not repecting db.dropScript in properties files
data/bin/sdm ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'sdm'
3
+
4
+ sdm = Sdm::Start.new
5
+ sdm.run
data/lib/sdm.rb ADDED
@@ -0,0 +1,239 @@
1
+ # local
2
+ require File.expand_path('../sdm/color', __FILE__)
3
+ require File.expand_path('../sdm/config', __FILE__)
4
+ require File.expand_path('../sdm/dir', __FILE__)
5
+ require File.expand_path('../trollop', __FILE__)
6
+
7
+ module Sdm
8
+
9
+ SUB_COMMANDS = %w(st status envs migrate exec execute x new drop mi)
10
+ SCHEMA_VERSIONS = "schema-versions"
11
+
12
+ class Start
13
+
14
+ def initialize
15
+ end
16
+
17
+ def environments
18
+ envs = Dir.entries(Sdm::SCHEMA_VERSIONS).delete_if do |f|
19
+ (f =~ /properties/) == nil || f == "base-db.properties"
20
+ end
21
+
22
+ envs.each { |x| x.gsub!(/\.properties/, '') }
23
+
24
+ puts "Environments to choose from:"
25
+ envs.each { |x| puts " " + x }
26
+ end
27
+
28
+ def mvn(task, opts)
29
+ env = ARGV.shift
30
+ properties_file = "#{Sdm::SCHEMA_VERSIONS}/#{env}.properties"
31
+
32
+ unless File.exists?(properties_file)
33
+ Trollop::die "Bad environment given"
34
+ end
35
+
36
+ config = parseconf("#{Sdm::SCHEMA_VERSIONS}/base-db.properties")
37
+ config.merge!(parseconf(properties_file))
38
+ schema = config['owningSchema']
39
+ database = config['database']
40
+ password = `pw get #{schema} #{database}`.chomp!
41
+
42
+
43
+ cmd = "mvn -q " +
44
+ "-Ddb.configurationFile=#{env}.properties " +
45
+ "-Doracle.net.tns_admin=$TNS_ADMIN "
46
+
47
+ # use specific schema or all specified in properties file
48
+ if s = opts[:schema]
49
+ cmd += "-Dschemas=#{s} "
50
+ cmd += "-D#{s}.password=#{password} "
51
+ if tv = opts[:target]
52
+ cmd += "-D#{s}.targetVersion=#{tv} "
53
+ end
54
+ else
55
+ config["schemas"].split(",").each do |s|
56
+ s.strip!
57
+ cmd += "-D#{s}.password=#{password} "
58
+ end
59
+ end
60
+
61
+ case task
62
+ when "execute"
63
+ script = ARGV.shift
64
+ cmd += "-Ddb.scriptToExecute=#{script} "
65
+ when "drop"
66
+ cmd += "-Ddb.dropScript=#{config["db.dropScript"]} "
67
+ end
68
+
69
+
70
+ cmd += "stack-db:#{task}"
71
+
72
+ system cmd
73
+ end
74
+
75
+ def new(opts)
76
+ s = opts[:schema]
77
+ name = ARGV.shift
78
+ cmd = "mvn -q " +
79
+ "-Ddb.configurationFile=pom.properties " + # must define one
80
+ "-Dschemas=#{s} " +
81
+ "-D#{s}.password=blah " + # hangs if none is given
82
+ "-Ddescription=\"#{name}\" " +
83
+ "stack-db:new"
84
+
85
+ system cmd
86
+ end
87
+
88
+ def run
89
+ unless Dir.chdir_to_parent_containing(Sdm::SCHEMA_VERSIONS)
90
+ puts "ERROR: ".red +
91
+ "Not a db migrator directory " +
92
+ "(nor any of its parent directories)"
93
+ exit 1
94
+ end
95
+
96
+ global = Trollop::Parser.new do
97
+ banner "Stack DB Migrator Helper".green
98
+ banner ""
99
+ banner <<-EOS
100
+ Wraps the mvn command for the [LDS Stack DB Migrator][1] to make it easier to use.
101
+
102
+ [1]: http://code.lds.org/maven-sites/stack/module.html?module=db-migrator
103
+
104
+ Commands:
105
+
106
+ envs Show available environments to run migrations on.
107
+ status st Display status of database. See pending migrations.
108
+ migrate mi Apply scripts in queue to bring database to target.
109
+ execute exec x Run specified script ad hoc. Without logging.
110
+ new Create a new blank script with timestamp in name.
111
+ drop Delete all objects in the database.
112
+
113
+ See `sdm <command> -h` to get additional help and usage on a specific command.
114
+
115
+ Global Options:
116
+ EOS
117
+ banner ""
118
+ version "0.5 Beta"
119
+ stop_on Sdm::SUB_COMMANDS
120
+ end
121
+
122
+ global_opts = Trollop::with_standard_exception_handling global do
123
+ o = global.parse ARGV
124
+ raise Trollop::HelpNeeded if ARGV.empty?
125
+ o
126
+ end
127
+
128
+ cmd = ARGV.shift # get the subcommand
129
+ case cmd
130
+ when "status", "st"
131
+ opts = Trollop::options do
132
+ banner "status: mvn stack-db:status".green
133
+ banner ""
134
+ banner <<-EOS
135
+ usage: sdm status ENV [-s SCHEMA]
136
+
137
+ Check the status of a database environment. Returns status of each schema listed in the 'schemas' attribute of the properties file for the environment, unless a schema is specified with the command.
138
+
139
+ Example: sdm status stg -s DEFAULT
140
+ EOS
141
+ banner ""
142
+ opt :schema, "Specify schema. Default is read from schemas property.",
143
+ :short => "-s", :type => :string
144
+ end
145
+ mvn("status", opts)
146
+ when "migrate", "mi"
147
+ opts = Trollop::options do
148
+ banner "migrate: mvn stack-db:migrate".green
149
+ banner ""
150
+ banner <<-EOS
151
+ usage: sdm migrate ENV [-s SCHEMA] [-t VERSION_NUMBER]
152
+
153
+ Apply scripts found in the schemas' queues to bring a database to the target version number.
154
+
155
+ Example: sdm migrate stg -s DEFAULT -t 201205151242
156
+ EOS
157
+ banner ""
158
+ opt :schema, "Specify schema. Default is read from schemas property.",
159
+ :short => "-s", :type => :string
160
+ opt :target, "Target version. Requires schema.",
161
+ :short => "-t", :type => :int
162
+ end
163
+ if opts[:target] && !opts[:schema]
164
+ Trollop::die :schema, "required when target specified"
165
+ end
166
+ mvn("migrate", opts)
167
+ when "execute", "exec", "x"
168
+ opts = Trollop::options do
169
+ banner "execute: mvn stack-db:execute".green
170
+ banner ""
171
+ banner <<-EOS
172
+ usage: sdm execute ENV -s SCHEMA SCRIPT
173
+
174
+ Run a specified SQL script. Execution is not logged. Script must exist in the schema directory specified.
175
+
176
+ Example: sdm execute stg -s DEFAULT test.sql
177
+ EOS
178
+ banner ""
179
+ opt :schema, "Specify schema. Required.",
180
+ :short => "-s", :type => :string, :required => true
181
+ end
182
+ mvn("execute", opts)
183
+ when "drop"
184
+ opts = Trollop::options do
185
+ banner "drop: mvn stack-db:drop".green
186
+ banner ""
187
+ banner <<-EOS
188
+ usage: sdm drop ENV [-s SCHEMA]
189
+
190
+ Delete all objects from the database. Will run on all schemas in the properties file unless a SCHEMA is specified. Uses the db.dropScript property to identify what script to run. If that is not set it uses the built-in drop script.
191
+
192
+ Example: sdm drop stg -s DEFAULT
193
+ EOS
194
+ banner ""
195
+ opt :schema, "Specify schema.",
196
+ :short => "-s", :type => :string
197
+ end
198
+ mvn("drop", opts)
199
+ when "new"
200
+ opts = Trollop::options do
201
+ banner "new: mvn stack-db:new".green
202
+ banner ""
203
+ banner <<-EOS
204
+ usage: sdm new -s SCHEMA [NAME_OF_SCRIPT]
205
+
206
+ Create a new migration file in a schema. The current timestamp is prefixed to the name of the file.
207
+
208
+ Example: sdm new -s DEFAULT "update a table"
209
+
210
+ EOS
211
+ banner ""
212
+ opt :schema, "Specify schema. Required.",
213
+ :short => "-s", :type => :string, :required => true
214
+ end
215
+ new(opts)
216
+ when "envs"
217
+ opts = Trollop::options do
218
+ banner "envs: List available environments.".green
219
+ banner ""
220
+ banner <<-EOS
221
+ usage: sdm envs
222
+
223
+ Lists names of environments that can be used in other commands. Each environments corresponds to a properties file in the schema-versions directory.
224
+
225
+ Example: sdm envs
226
+ EOS
227
+ banner ""
228
+ end
229
+ environments
230
+ else
231
+ puts "Error: ".red + "Unknown command #{cmd.inspect}."
232
+ puts "See 'sdm --help'."
233
+ exit 1
234
+ end
235
+
236
+ end
237
+
238
+ end
239
+ end
data/lib/sdm/color.rb ADDED
@@ -0,0 +1,11 @@
1
+ # Extend String to add color
2
+ class String
3
+ def green
4
+ "" + self + ""
5
+ end
6
+ def red
7
+ "" + self + ""
8
+ end
9
+ end
10
+
11
+
data/lib/sdm/config.rb ADDED
@@ -0,0 +1,25 @@
1
+ # based from http://www.erickcantwell.com/code/config.rb
2
+ def parseconf(conf_file)
3
+ props = {}
4
+
5
+ unless File.exists?(conf_file)
6
+ raise "Properties file not found: #{conf_file}."
7
+ end
8
+
9
+ IO.foreach(conf_file) do |line|
10
+ next if line.match(/^#/)
11
+ next if line.match(/^$/)
12
+
13
+ if line.match(/=/)
14
+ eq = line.index("=")
15
+
16
+ key = line[0..eq-1].strip
17
+ value = line[eq+1..-1].strip
18
+
19
+ props[key] = value
20
+ end
21
+ end
22
+
23
+ return props
24
+ end
25
+
data/lib/sdm/dir.rb ADDED
@@ -0,0 +1,24 @@
1
+ class Dir
2
+
3
+ class << self
4
+ def chdir_to_parent_containing(file)
5
+
6
+ dir = Dir.pwd.split("/")
7
+
8
+ (dir.size - 1).times do |x|
9
+ d = dir.join("/")
10
+
11
+ if Dir.entries(d).include?(file)
12
+ Dir.chdir(d)
13
+ return true
14
+ end
15
+
16
+ dir.pop
17
+ end
18
+
19
+ return false
20
+
21
+ end
22
+ end
23
+
24
+ end
data/lib/trollop.rb ADDED
@@ -0,0 +1,783 @@
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 File.expand_path('../sdm/color', __FILE__)
8
+
9
+ module Trollop
10
+
11
+ VERSION = "1.16.2"
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+))([eE][-+]?[\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 you want to instantiate this class yourself (for more complicated
37
+ ## argument-parsing logic), call #parse to actually produce the output hash,
38
+ ## and consider calling it from within
39
+ ## Trollop::with_standard_exception_handling.
40
+ class Parser
41
+
42
+ ## The set of values that indicate a flag option when passed as the
43
+ ## +:type+ parameter of #opt.
44
+ FLAG_TYPES = [:flag, :bool, :boolean]
45
+
46
+ ## The set of values that indicate a single-parameter (normal) option when
47
+ ## passed as the +:type+ parameter of #opt.
48
+ ##
49
+ ## A value of +io+ corresponds to a readable IO resource, including
50
+ ## a filename, URI, or the strings 'stdin' or '-'.
51
+ SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
52
+
53
+ ## The set of values that indicate a multiple-parameter option (i.e., that
54
+ ## takes multiple space-separated values on the commandline) when passed as
55
+ ## the +:type+ parameter of #opt.
56
+ MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
57
+
58
+ ## The complete set of legal values for the +:type+ parameter of #opt.
59
+ TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
60
+
61
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
62
+
63
+ ## The values from the commandline that were not interpreted by #parse.
64
+ attr_reader :leftovers
65
+
66
+ ## The complete configuration hashes for each option. (Mainly useful
67
+ ## for testing.)
68
+ attr_reader :specs
69
+
70
+ ## Initializes the parser, and instance-evaluates any block given.
71
+ def initialize *a, &b
72
+ @version = nil
73
+ @leftovers = []
74
+ @specs = {}
75
+ @long = {}
76
+ @short = {}
77
+ @order = []
78
+ @constraints = []
79
+ @stop_words = []
80
+ @stop_on_unknown = false
81
+
82
+ #instance_eval(&b) if b # can't take arguments
83
+ cloaker(&b).bind(self).call(*a) if b
84
+ end
85
+
86
+ ## Define an option. +name+ is the option name, a unique identifier
87
+ ## for the option that you will use internally, which should be a
88
+ ## symbol or a string. +desc+ is a string description which will be
89
+ ## displayed in help messages.
90
+ ##
91
+ ## Takes the following optional arguments:
92
+ ##
93
+ ## [+: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.
94
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
95
+ ## [+: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.
96
+ ## [+: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+.
97
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
98
+ ## [+: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.)
99
+ ##
100
+ ## Note that there are two types of argument multiplicity: an argument
101
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
102
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
103
+ ##
104
+ ## Arguments that take multiple values should have a +:type+ parameter
105
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
106
+ ## value of an array of the correct type (e.g. [String]). The
107
+ ## value of this argument will be an array of the parameters on the
108
+ ## commandline.
109
+ ##
110
+ ## Arguments that can occur multiple times should be marked with
111
+ ## +:multi+ => +true+. The value of this argument will also be an array.
112
+ ## In contrast with regular non-multi options, if not specified on
113
+ ## the commandline, the default value will be [], not nil.
114
+ ##
115
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
116
+ ## +:multi+ => +true+), in which case the value of the argument will be
117
+ ## an array of arrays.
118
+ ##
119
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
120
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
121
+ ## is a multi-value argument as well as a multi-occurrence argument.
122
+ ## In thise case, Trollop assumes that it's not a multi-value argument.
123
+ ## If you want a multi-value, multi-occurrence argument with a default
124
+ ## value, you must specify +:type+ as well.
125
+
126
+ def opt name, desc="", opts={}
127
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
128
+
129
+ ## fill in :type
130
+ opts[:type] = # normalize
131
+ case opts[:type]
132
+ when :boolean, :bool; :flag
133
+ when :integer; :int
134
+ when :integers; :ints
135
+ when :double; :float
136
+ when :doubles; :floats
137
+ when Class
138
+ case opts[:type].name
139
+ when 'TrueClass', 'FalseClass'; :flag
140
+ when 'String'; :string
141
+ when 'Integer'; :int
142
+ when 'Float'; :float
143
+ when 'IO'; :io
144
+ when 'Date'; :date
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 Array
174
+ if opts[:default].empty?
175
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
176
+ end
177
+ case opts[:default][0] # the first element determines the types
178
+ when Integer; :ints
179
+ when Numeric; :floats
180
+ when String; :strings
181
+ when IO; :ios
182
+ when Date; :dates
183
+ else
184
+ raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
185
+ end
186
+ when nil; nil
187
+ else
188
+ raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
189
+ end
190
+
191
+ 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
192
+
193
+ opts[:type] = opts[:type] || type_from_default || :flag
194
+
195
+ ## fill in :long
196
+ opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
197
+ opts[:long] =
198
+ case opts[:long]
199
+ when /^--([^-].*)$/
200
+ $1
201
+ when /^[^-]/
202
+ opts[:long]
203
+ else
204
+ raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
205
+ end
206
+ raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
207
+
208
+ ## fill in :short
209
+ opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
210
+ opts[:short] = case opts[:short]
211
+ when /^-(.)$/; $1
212
+ when nil, :none, /^.$/; opts[:short]
213
+ else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
214
+ end
215
+
216
+ if opts[:short]
217
+ raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
218
+ raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
219
+ end
220
+
221
+ ## fill in :default for flags
222
+ opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
223
+
224
+ ## autobox :default for :multi (multi-occurrence) arguments
225
+ opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
226
+
227
+ ## fill in :multi
228
+ opts[:multi] ||= false
229
+
230
+ opts[:desc] ||= desc
231
+ @long[opts[:long]] = name
232
+ @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
233
+ @specs[name] = opts
234
+ @order << [:opt, name]
235
+ end
236
+
237
+ ## Sets the version string. If set, the user can request the version
238
+ ## on the commandline. Should probably be of the form "<program name>
239
+ ## <version number>".
240
+ def version s=nil; @version = s if s; @version end
241
+
242
+ ## Adds text to the help display. Can be interspersed with calls to
243
+ ## #opt to build a multi-section help page.
244
+ def banner s; @order << [:text, s] end
245
+ alias :text :banner
246
+
247
+ ## Marks two (or more!) options as requiring each other. Only handles
248
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
249
+ ## better modeled with Trollop::die.
250
+ def depends *syms
251
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
252
+ @constraints << [:depends, syms]
253
+ end
254
+
255
+ ## Marks two (or more!) options as conflicting.
256
+ def conflicts *syms
257
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
258
+ @constraints << [:conflicts, syms]
259
+ end
260
+
261
+ ## Defines a set of words which cause parsing to terminate when
262
+ ## encountered, such that any options to the left of the word are
263
+ ## parsed as usual, and options to the right of the word are left
264
+ ## intact.
265
+ ##
266
+ ## A typical use case would be for subcommand support, where these
267
+ ## would be set to the list of subcommands. A subsequent Trollop
268
+ ## invocation would then be used to parse subcommand options, after
269
+ ## shifting the subcommand off of ARGV.
270
+ def stop_on *words
271
+ @stop_words = [*words].flatten
272
+ end
273
+
274
+ ## Similar to #stop_on, but stops on any unknown word when encountered
275
+ ## (unless it is a parameter for an argument). This is useful for
276
+ ## cases where you don't know the set of subcommands ahead of time,
277
+ ## i.e., without first parsing the global options.
278
+ def stop_on_unknown
279
+ @stop_on_unknown = true
280
+ end
281
+
282
+ ## Parses the commandline. Typically called by Trollop::options,
283
+ ## but you can call it directly if you need more control.
284
+ ##
285
+ ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
286
+ def parse cmdline=ARGV
287
+ vals = {}
288
+ required = {}
289
+
290
+ opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
291
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
292
+
293
+ @specs.each do |sym, opts|
294
+ required[sym] = true if opts[:required]
295
+ vals[sym] = opts[:default]
296
+ vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
297
+ end
298
+
299
+ resolve_default_short_options
300
+
301
+ ## resolve symbols
302
+ given_args = {}
303
+ @leftovers = each_arg cmdline do |arg, params|
304
+ sym = case arg
305
+ when /^-([^-])$/
306
+ @short[$1]
307
+ when /^--([^-]\S*)$/
308
+ @long[$1]
309
+ else
310
+ raise CommandlineError, "invalid argument syntax: '#{arg}'"
311
+ end
312
+ raise CommandlineError, "unknown argument '#{arg}'" unless sym
313
+
314
+ if given_args.include?(sym) && !@specs[sym][:multi]
315
+ raise CommandlineError, "option '#{arg}' specified multiple times"
316
+ end
317
+
318
+ given_args[sym] ||= {}
319
+
320
+ given_args[sym][:arg] = arg
321
+ given_args[sym][:params] ||= []
322
+
323
+ # The block returns the number of parameters taken.
324
+ num_params_taken = 0
325
+
326
+ unless params.nil?
327
+ if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
328
+ given_args[sym][:params] << params[0, 1] # take the first parameter
329
+ num_params_taken = 1
330
+ elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
331
+ given_args[sym][:params] << params # take all the parameters
332
+ num_params_taken = params.size
333
+ end
334
+ end
335
+
336
+ num_params_taken
337
+ end
338
+
339
+ ## check for version and help args
340
+ raise VersionNeeded if given_args.include? :version
341
+ raise HelpNeeded if given_args.include? :help
342
+
343
+ ## check constraint satisfaction
344
+ @constraints.each do |type, syms|
345
+ constraint_sym = syms.find { |sym| given_args[sym] }
346
+ next unless constraint_sym
347
+
348
+ case type
349
+ when :depends
350
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
351
+ when :conflicts
352
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
353
+ end
354
+ end
355
+
356
+ required.each do |sym, val|
357
+ raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
358
+ end
359
+
360
+ ## parse parameters
361
+ given_args.each do |sym, given_data|
362
+ arg = given_data[:arg]
363
+ params = given_data[:params]
364
+
365
+ opts = @specs[sym]
366
+ raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
367
+
368
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
369
+
370
+ case opts[:type]
371
+ when :flag
372
+ vals[sym] = !opts[:default]
373
+ when :int, :ints
374
+ vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
375
+ when :float, :floats
376
+ vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
377
+ when :string, :strings
378
+ vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
379
+ when :io, :ios
380
+ vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
381
+ when :date, :dates
382
+ vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
383
+ end
384
+
385
+ if SINGLE_ARG_TYPES.include?(opts[:type])
386
+ unless opts[:multi] # single parameter
387
+ vals[sym] = vals[sym][0][0]
388
+ else # multiple options, each with a single parameter
389
+ vals[sym] = vals[sym].map { |p| p[0] }
390
+ end
391
+ elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
392
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
393
+ end
394
+ # else: multiple options, with multiple parameters
395
+ end
396
+
397
+ ## modify input in place with only those
398
+ ## arguments we didn't process
399
+ cmdline.clear
400
+ @leftovers.each { |l| cmdline << l }
401
+
402
+ ## allow openstruct-style accessors
403
+ class << vals
404
+ def method_missing(m, *args)
405
+ self[m] || self[m.to_s]
406
+ end
407
+ end
408
+ vals
409
+ end
410
+
411
+ def parse_date_parameter param, arg #:nodoc:
412
+ begin
413
+ begin
414
+ time = Chronic.parse(param)
415
+ rescue NameError
416
+ # chronic is not available
417
+ end
418
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
419
+ rescue ArgumentError => e
420
+ raise CommandlineError, "option '#{arg}' needs a date"
421
+ end
422
+ end
423
+
424
+ ## Print the help message to +stream+.
425
+ def educate stream=$stdout
426
+ width # just calculate it now; otherwise we have to be careful not to
427
+ # call this unless the cursor's at the beginning of a line.
428
+
429
+ left = {}
430
+ @specs.each do |name, spec|
431
+ left[name] = "--#{spec[:long]}" +
432
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
433
+ case spec[:type]
434
+ when :flag; ""
435
+ when :int; " <i>"
436
+ when :ints; " <i+>"
437
+ when :string; " <s>"
438
+ when :strings; " <s+>"
439
+ when :float; " <f>"
440
+ when :floats; " <f+>"
441
+ when :io; " <filename/uri>"
442
+ when :ios; " <filename/uri+>"
443
+ when :date; " <date>"
444
+ when :dates; " <date+>"
445
+ end
446
+ end
447
+
448
+ leftcol_width = left.values.map { |s| s.length }.max || 0
449
+ rightcol_start = leftcol_width + 6 # spaces
450
+
451
+ unless @order.size > 0 && @order.first.first == :text
452
+ stream.puts "#@version\n" if @version
453
+ stream.puts "Options:"
454
+ end
455
+
456
+ @order.each do |what, opt|
457
+ if what == :text
458
+ stream.puts wrap(opt)
459
+ next
460
+ end
461
+
462
+ spec = @specs[opt]
463
+ stream.printf " %#{leftcol_width}s: ", left[opt]
464
+ desc = spec[:desc] + begin
465
+ default_s = case spec[:default]
466
+ when $stdout; "<stdout>"
467
+ when $stdin; "<stdin>"
468
+ when $stderr; "<stderr>"
469
+ when Array
470
+ spec[:default].join(", ")
471
+ else
472
+ spec[:default].to_s
473
+ end
474
+
475
+ if spec[:default]
476
+ if spec[:desc] =~ /\.$/
477
+ " (Default: #{default_s})"
478
+ else
479
+ " (default: #{default_s})"
480
+ end
481
+ else
482
+ ""
483
+ end
484
+ end
485
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
486
+ end
487
+ end
488
+
489
+ def width #:nodoc:
490
+ @width ||= if $stdout.tty?
491
+ begin
492
+ require 'curses'
493
+ Curses::init_screen
494
+ x = Curses::cols
495
+ Curses::close_screen
496
+ x
497
+ rescue Exception
498
+ 80
499
+ end
500
+ else
501
+ 80
502
+ end
503
+ end
504
+
505
+ def wrap str, opts={} # :nodoc:
506
+ if str == ""
507
+ [""]
508
+ else
509
+ str.split("\n").map { |s| wrap_line s, opts }.flatten
510
+ end
511
+ end
512
+
513
+ ## The per-parser version of Trollop::die (see that for documentation).
514
+ def die arg, msg
515
+ if msg
516
+ $stderr.puts "Error: ".red + "argument --#{@specs[arg][:long]} #{msg}."
517
+ else
518
+ $stderr.puts "Error: ".red + "#{arg}."
519
+ end
520
+ $stderr.puts "Try --help for help."
521
+ exit(-1)
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 easy, syntactic-sugary entry method into Trollop. Creates a Parser,
678
+ ## passes the block to it, then parses +args+ with it, handling any errors or
679
+ ## requests for help or version information appropriately (and then exiting).
680
+ ## Modifies +args+ in place. Returns a hash of option values.
681
+ ##
682
+ ## The block passed in should contain zero or more calls to +opt+
683
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
684
+ ## probably a call to +version+ (Parser#version).
685
+ ##
686
+ ## The returned block contains a value for every option specified with
687
+ ## +opt+. The value will be the value given on the commandline, or the
688
+ ## default value if the option was not specified on the commandline. For
689
+ ## every option specified on the commandline, a key "<option
690
+ ## name>_given" will also be set in the hash.
691
+ ##
692
+ ## Example:
693
+ ##
694
+ ## require 'trollop'
695
+ ## opts = Trollop::options do
696
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
697
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
698
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
699
+ ## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
700
+ ## end
701
+ ##
702
+ ## ## if called with no arguments
703
+ ## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
704
+ ##
705
+ ## ## if called with --monkey
706
+ ## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
707
+ ##
708
+ ## See more examples at http://trollop.rubyforge.org.
709
+ def options args=ARGV, *a, &b
710
+ @last_parser = Parser.new(*a, &b)
711
+ with_standard_exception_handling(@last_parser) { @last_parser.parse args }
712
+ end
713
+
714
+ ## If Trollop::options doesn't do quite what you want, you can create a Parser
715
+ ## object and call Parser#parse on it. That method will throw CommandlineError,
716
+ ## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
717
+ ## have these handled for you in the standard manner (e.g. show the help
718
+ ## and then exit upon an HelpNeeded exception), call your code from within
719
+ ## a block passed to this method.
720
+ ##
721
+ ## Note that this method will call System#exit after handling an exception!
722
+ ##
723
+ ## Usage example:
724
+ ##
725
+ ## require 'trollop'
726
+ ## p = Trollop::Parser.new do
727
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
728
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
729
+ ## end
730
+ ##
731
+ ## opts = Trollop::with_standard_exception_handling p do
732
+ ## o = p.parse ARGV
733
+ ## raise Trollop::HelpNeeded if ARGV.empty? # show help screen
734
+ ## o
735
+ ## end
736
+ ##
737
+ ## Requires passing in the parser object.
738
+
739
+ def with_standard_exception_handling parser
740
+ begin
741
+ yield
742
+ rescue CommandlineError => e
743
+ $stderr.puts "Error: ".red + "#{e.message}."
744
+ $stderr.puts "Try --help for help."
745
+ exit(-1)
746
+ rescue HelpNeeded
747
+ parser.educate
748
+ exit
749
+ rescue VersionNeeded
750
+ puts parser.version
751
+ exit
752
+ end
753
+ end
754
+
755
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
756
+ ## 'msg', and dies. Example:
757
+ ##
758
+ ## options do
759
+ ## opt :volume, :default => 0.0
760
+ ## end
761
+ ##
762
+ ## die :volume, "too loud" if opts[:volume] > 10.0
763
+ ## die :volume, "too soft" if opts[:volume] < 0.1
764
+ ##
765
+ ## In the one-argument case, simply print that message, a notice
766
+ ## about -h, and die. Example:
767
+ ##
768
+ ## options do
769
+ ## opt :whatever # ...
770
+ ## end
771
+ ##
772
+ ## Trollop::die "need at least one filename" if ARGV.empty?
773
+ def die arg, msg=nil
774
+ if @last_parser
775
+ @last_parser.die arg, msg
776
+ else
777
+ raise ArgumentError, "Trollop::die can only be called after Trollop::options"
778
+ end
779
+ end
780
+
781
+ module_function :options, :die, :with_standard_exception_handling
782
+
783
+ end # module
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sdm
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 5
8
+ version: "0.5"
9
+ platform: ruby
10
+ authors:
11
+ - Miles Pomeroy
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2012-05-22 00:00:00 -06:00
17
+ default_executable:
18
+ dependencies: []
19
+
20
+ description: Wraps the mvn command for the LDS Stack DB Migrator to make it easier to use.
21
+ email: miles@nonsensequel.com
22
+ executables:
23
+ - sdm
24
+ extensions: []
25
+
26
+ extra_rdoc_files: []
27
+
28
+ files:
29
+ - README
30
+ - TODO
31
+ - bin/sdm
32
+ - lib/trollop.rb
33
+ - lib/sdm.rb
34
+ - lib/sdm/color.rb
35
+ - lib/sdm/config.rb
36
+ - lib/sdm/dir.rb
37
+ has_rdoc: true
38
+ homepage: https://github.com/milespomeroy/sdm
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.6
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: LDS Stack DB Migrator helper
67
+ test_files: []
68
+