Class: SiteFuel::External::AbstractExternalProgram
- Includes:
- Logging
- Defined in:
- lib/sitefuel/external/AbstractExternalProgram.rb
Overview
lightweight abstraction around a program external to Ruby. The class is designed to make it easy to use an external program in a batch fashion. Note that the abstraction does not well support interacting back and forth with external programs.
Constant Summary collapse
- VERSION_SEPARATOR =
'.'
- @@compatible_versions =
cache of whether compatible versions of programs exist
{}
- @@program_exists =
cache of whether the actual programs that are abstracted exist
{}
- @@program_binary =
{}
- @@program_options =
{}
- @@option_struct =
Struct.new('ExternalProgramOption', 'name', 'template', 'default')
Instance Attribute Summary collapse
-
#options ⇒ Object
readonly
INSTANCE METHODS.
Class Method Summary collapse
-
.allowed_option_name?(name) ⇒ Boolean
tests whether a given option name is allowed.
-
.call_option(option_name) ⇒ Object
calls an option.
-
.capture_output(command, *args) ⇒ Object
Similar to Kernel#exec, but returns a string of the output.
-
.capture_stderr ⇒ Object
generally we don’t want to capture stderr since it helps users with finding out why things don’t work.
-
.compatible_version? ⇒ Boolean
gives true if a binary with a compatible version exists.
-
.compatible_version_number?(version_number) ⇒ Boolean
gives true if a given version number is compatible.
-
.compatible_versions ⇒ Object
gives a condition on the compatible versions.
-
.create_tmp_directory(keyword) ⇒ Object
creates a temporary directory for sitefuel.
-
.ensure_valid_option(name) ⇒ Object
raises UnknownOption error if the given option isn’t valid.
-
.excluded_option_names ⇒ Object
list of excluded option names.
-
.execute(*options) ⇒ Object
creates and executes an instance of this external program by taking a list of parameters and their values.
-
.extract_program_version(version_output) ⇒ Object
given the output of a program gives the version number or nil if not available.
-
.option(name, template = nil, default = nil) ⇒ Object
declares an option for this program.
-
.option?(name) ⇒ Boolean
gives true if given a known option.
-
.option_default(option_name) ⇒ Object
gives the default value for an option.
-
.option_template(option_name) ⇒ Object
gives the template for an option.
-
.option_version ⇒ Object
option for giving the version of the program.
-
.options ⇒ Object
gives the listing of declared options for the program.
-
.organize_options(*options) ⇒ Object
organizes a list of options into a ragged array of arrays.
-
.output_handling ⇒ Object
- controls what happens with the output from the program =capture=
- output is captured into a string and returned from #execute =forward=
-
output is forwarded to the terminal as normal.
-
.program_binary ⇒ Object
gives the location of the external program; uses the =which= unix command.
-
.program_found? ⇒ Boolean
gives true if the program can be found.
-
.program_version ⇒ Object
gets the version of a program.
-
.random_string(length = 12) ⇒ Object
creates a random string by hashing the current time into hexadecimal.
-
.test_version_number(compatible, version_number) ⇒ Object
tests a version number against a list of compatible version specifications should be made into a Version class.
-
.verify_compatible_version ⇒ Object
raises the ProgramNotFound error if the program can’t be found raises the VersionNotFound error if a compatible version isn’t found.
-
.verify_program_exists ⇒ Object
raises the ProgramNotFound error if the program can’t be found See also AbstractExternalProgram.verify_compatible_version.
-
.version_older?(lhs, rhs) ⇒ Boolean
gives true if the given version is newer.
Instance Method Summary collapse
-
#add_option(option_row) ⇒ Object
adds an option to be passed to this instance.
-
#apply_value(option_template, value) ⇒ Object
applies a given value into an option template.
-
#build_command_line ⇒ Object
builds the command line for a given program instance.
-
#ensure_option_validity(option_row) ⇒ Object
ensures the option specification makes sense.
-
#execute ⇒ Object
executes the given AbstractExternalProgram instance.
-
#has_default?(name) ⇒ Boolean
gives true if an option has a default.
-
#initialize ⇒ AbstractExternalProgram
constructor
A new instance of AbstractExternalProgram.
-
#option_template(name) ⇒ Object
gives the template for an option.
-
#requires_value?(name) ⇒ Boolean
gives true if an option takes a value but has no default.
-
#takes_value?(name) ⇒ Boolean
returns true if a given option takes a value TODO this should be precomputed.
-
#valid_option?(option_row) ⇒ Boolean
gives true if the given option is valid.
Methods included from Logging
#debug, #error, #fatal, #info, #logger=, #warn
Constructor Details
#initialize ⇒ AbstractExternalProgram
Returns a new instance of AbstractExternalProgram.
539 540 541 542 543 544 545 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 539 def initialize # check that a compatible version exists self.class.verify_compatible_version self.logger = SiteFuelLogger.instance @options = [] end |
Instance Attribute Details
#options ⇒ Object (readonly)
INSTANCE METHODS
537 538 539 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 537 def @options end |
Class Method Details
.allowed_option_name?(name) ⇒ Boolean
tests whether a given option name is allowed
396 397 398 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 396 def self.allowed_option_name?(name) not excluded_option_names.include?(name.to_sym) end |
.call_option(option_name) ⇒ Object
calls an option
364 365 366 367 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 364 def self.call_option(option_name) method_name = "option_"+option_name.to_s self.send(method_name.to_sym) end |
.capture_output(command, *args) ⇒ Object
Similar to Kernel#exec, but returns a string of the output
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 207 def self.capture_output(command, *args) cli = command + ' ' + args.join(' ') # if we want to capture stderr we need to redirect to stdout if capture_stderr cli << ' 2>&1' end output_string = "" IO.popen(cli, 'r') do |io| output_string = io.read.chop end output_string end |
.capture_stderr ⇒ Object
generally we don’t want to capture stderr since it helps users with finding out why things don’t work. In certain cases we do need to capture it, however.
588 589 590 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 588 def self.capture_stderr false end |
.compatible_version? ⇒ Boolean
gives true if a binary with a compatible version exists
243 244 245 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 243 def self.compatible_version? compatible_version_number?(program_version) end |
.compatible_version_number?(version_number) ⇒ Boolean
gives true if a given version number is compatible
300 301 302 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 300 def self.compatible_version_number?(version_number) self.test_version_number(compatible_versions, version_number) end |
.compatible_versions ⇒ Object
gives a condition on the compatible versions. A version is considered compatible if it’s greater than the given version. Eventually we’ll probably need a way to give a version range and allow excluding particular versions.
237 238 239 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 237 def self.compatible_versions '> 0.0.0' end |
.create_tmp_directory(keyword) ⇒ Object
creates a temporary directory for sitefuel
522 523 524 525 526 527 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 522 def self.create_tmp_directory(keyword) dir_name = File.join(Dir.tmpdir, "sitefuel-#{keyword}-#{random_string}") Dir.mkdir(dir_name) dir_name end |
.ensure_valid_option(name) ⇒ Object
raises UnknownOption error if the given option isn’t valid
422 423 424 425 426 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 422 def self.ensure_valid_option(name) if not option?(name) raise UnknownOption.new(self, name) end end |
.excluded_option_names ⇒ Object
list of excluded option names
390 391 392 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 390 def self.excluded_option_names [:default, :template] end |
.execute(*options) ⇒ Object
creates and executes an instance of this external program by taking a list of parameters and their values
self.execute :setflag, # set a flag
:paramsetting, "param value", # pass a single value
:paramsetting2, "val1", "val2" # pass multiple values
503 504 505 506 507 508 509 510 511 512 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 503 def self.execute(*) instance = self.new organized = (*) organized.each do |opt| instance.add_option(opt) end instance.execute end |
.extract_program_version(version_output) ⇒ Object
given the output of a program gives the version number or nil if not available
344 345 346 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 344 def self.extract_program_version(version_output) version_output[/(\d+\.\d+(\.\d+)?([a-zA-Z]+)?)/] end |
.option(name, template = nil, default = nil) ⇒ Object
declares an option for this program
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 430 def self.option(name, template = nil, default = nil) unless name.kind_of? String name = name.to_s end unless allowed_option_name?(name) raise UnallowedOptionName.new(self, name, excluded_option_names) end # if a default is given but the template has no value slot... if default != nil and not template.include?('${value}') raise NoOptionValueSlot.new(self, name) end # give a method for the option method_name = "option_"+name struct = @@option_struct.new(name, template, default) define_class_method(method_name.to_sym) { struct } end |
.option?(name) ⇒ Boolean
gives true if given a known option
416 417 418 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 416 def self.option?(name) self..include?(name) end |
.option_default(option_name) ⇒ Object
gives the default value for an option
402 403 404 405 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 402 def self.option_default(option_name) ensure_valid_option(option_name) self.call_option(option_name).default end |
.option_template(option_name) ⇒ Object
gives the template for an option
409 410 411 412 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 409 def self.option_template(option_name) ensure_valid_option(option_name) self.call_option(option_name).template end |
.option_version ⇒ Object
option for giving the version of the program
350 351 352 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 350 def self.option_version '--version' end |
.options ⇒ Object
gives the listing of declared options for the program
371 372 373 374 375 376 377 378 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 371 def self. names = methods names = names.delete_if { |method| not method =~ /^option_.*$/ } names.sort! names = names.map { |option_name| option_name.sub(/^option_(.*)$/, '\1').to_sym } names - excluded_option_names end |
.organize_options(*options) ⇒ Object
organizes a list of options into a ragged array of arrays
(:setflag, :paramsetting, 'val1', 'val2')
# =>[[:setflag], [:paramsetting, 'val1', 'val2']]
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 456 def self.(*) organized = [] i = 0 while i < .length # if we see a symbol are at a new option if [i].kind_of? Symbol option_row = [[i]] organized << option_row j = i+1 while j < .length case [j] when String, Fixnum, Float # adds this value option_row << [j] j += 1 when Symbol # the zoom below will cause this spot to get looked at break else # the zoom below will force us to look at this spot again # and bail break end end # zoom i ahead to this spot i = j else raise MalformedOptions.new(self, ) end end return organized end |
.output_handling ⇒ Object
controls what happens with the output from the program
capture=:: output is captured into a string and returned from #execute
forward=:: output is forwarded to the terminal as normal
384 385 386 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 384 def self.output_handling :capture end |
.program_binary ⇒ Object
gives the location of the external program; uses the =which= unix command
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 187 def self.program_binary # give the cached version if possible cached = @@program_binary[self] return cached if cached # otherwise try to find it: binary = capture_output("which", program_name) if binary.empty? raise ProgramNotFound.new(program_name) else # ensure the binary is resolved with respect to the root path binary = File.(binary, capture_output('pwd')) @@program_binary[self] = binary binary end end |
.program_found? ⇒ Boolean
gives true if the program can be found.
225 226 227 228 229 230 231 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 225 def self.program_found? program_binary rescue ProgramNotFound false else true end |
.program_version ⇒ Object
gets the version of a program
356 357 358 359 360 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 356 def self.program_version extract_program_version(capture_output(program_binary, option_version)) rescue ProgramNotFound return nil end |
.random_string(length = 12) ⇒ Object
creates a random string by hashing the current time into hexadecimal
516 517 518 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 516 def self.random_string(length=12) Digest::SHA1.hexdigest(Time.now.to_f.to_s)[0, length] end |
.test_version_number(compatible, version_number) ⇒ Object
tests a version number against a list of compatible version specifications should be made into a Version class. We could also expand the Gem::Version class and use that.…
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 277 def self.test_version_number(compatible, version_number) # ensure we're dealing with an array version_scheme = compatible if not version_scheme.kind_of? Array version_scheme = [version_scheme] end version_scheme.each do |ver| case ver[0..0] when '>' return version_older?(ver[1..-1].strip, version_number) when '<' return !version_older?(ver[1..-1].strip, version_number) else # ignore this version spec end end return false end |
.verify_compatible_version ⇒ Object
raises the ProgramNotFound error if the program can’t be found raises the VersionNotFound error if a compatible version isn’t found. the verification is cached using a class variable so the verification only actually happens the first time.
Because of the caching this function is generally very fast and should be called by every method that actually will execute the program.
327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 327 def self.verify_compatible_version verify_program_exists if @@compatible_versions[self] == nil @@compatible_versions[self] = compatible_version? end if @@compatible_versions[self] == true return true else raise VersionNotFound.new(self, self.compatible_versions, self.program_version) end end |
.verify_program_exists ⇒ Object
raises the ProgramNotFound error if the program can’t be found See also AbstractExternalProgram.verify_compatible_version
307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 307 def self.verify_program_exists if @@program_exists[self] == nil @@program_exists[self] = program_found? end if @@program_exists[self] == true return true else raise ProgramNotFound(self) end end |
.version_older?(lhs, rhs) ⇒ Boolean
gives true if the given version is newer. TODO this should be replaced by a proper version handling library (eg. versionometry (sp?))
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 251 def self.version_older? (lhs, rhs) return true if lhs == rhs # split into separate version chunks lhs = lhs.split(VERSION_SEPARATOR) rhs = rhs.split(VERSION_SEPARATOR) # if lhs is shorter than the rhs must be greater than or equal to the # lhs; but if the lhs is *longer* than the rhs must be greater than the # lhs. if lhs.length > rhs.length lhs = lhs[0...rhs.length] method = :< else method = :<= rhs = rhs[0...lhs.length] end # now compare lhs.join(VERSION_SEPARATOR).send(method, rhs.join(VERSION_SEPARATOR)) end |
Instance Method Details
#add_option(option_row) ⇒ Object
adds an option to be passed to this instance
569 570 571 572 573 574 575 576 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 569 def add_option(option_row) ensure_option_validity(option_row) case option_row when Array @options << option_row end end |
#apply_value(option_template, value) ⇒ Object
applies a given value into an option template
594 595 596 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 594 def apply_value(option_template, value) option_template.gsub('${value}', value.to_s) end |
#build_command_line ⇒ Object
builds the command line for a given program instance
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 619 def build_command_line self.class.verify_compatible_version exec_string = self.class.program_binary.clone @options.each do |option_row| option_string = option_template(option_row.first) case option_row.length when 1 if takes_value?(option_row.first) option_string = apply_value(option_string, self.class.option_default(option_row.first)) end when 2 option_string = apply_value(option_string, option_row[1]) else option_string = '' end exec_string << ' ' << option_string end exec_string end |
#ensure_option_validity(option_row) ⇒ Object
ensures the option specification makes sense
549 550 551 552 553 554 555 556 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 549 def ensure_option_validity(option_row) name = option_row.first if requires_value?(name) and option_row.length < 2 raise NoValueForOption.new(self.class, name) end true end |
#execute ⇒ Object
executes the given AbstractExternalProgram instance
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 645 def execute exec_string = build_command_line info ' Executing: '+exec_string output_handler = self.class.output_handling case output_handler when :capture output_string = self.class.capture_output(exec_string) when :forward output_string = exec(exec_string) else raise "Unknown output handler: #{output_handler}" end if $?.success? return output_string else raise ProgramExitedWithFailure.new(self.class, exec_string, $?.to_i) end end |
#has_default?(name) ⇒ Boolean
gives true if an option has a default
607 608 609 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 607 def has_default?(name) self.class.option_default(name) != nil end |
#option_template(name) ⇒ Object
gives the template for an option
580 581 582 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 580 def option_template(name) self.class.option_template(name) end |
#requires_value?(name) ⇒ Boolean
gives true if an option takes a value but has no default
613 614 615 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 613 def requires_value?(name) takes_value?(name) and not has_default?(name) end |
#takes_value?(name) ⇒ Boolean
returns true if a given option takes a value TODO this should be precomputed
601 602 603 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 601 def takes_value? (name) option_template(name).include?('${value}') end |
#valid_option?(option_row) ⇒ Boolean
gives true if the given option is valid
560 561 562 563 564 565 |
# File 'lib/sitefuel/external/AbstractExternalProgram.rb', line 560 def valid_option?(option_row) ensure_option_validity(option_row) return true rescue return false end |