rubycut-babushka 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +31 -0
  3. data/README.markdown +246 -0
  4. data/Rakefile +26 -0
  5. data/bin/babushka +11 -0
  6. data/deps/babushka.rb +101 -0
  7. data/deps/dev.rb +12 -0
  8. data/deps/fhs.rb +31 -0
  9. data/deps/git.rb +29 -0
  10. data/deps/homebrew.rb +30 -0
  11. data/deps/os_x.rb +33 -0
  12. data/deps/packages.rb +22 -0
  13. data/deps/pkg_managers.rb +110 -0
  14. data/deps/ruby.rb +23 -0
  15. data/deps/rubygems.rb +24 -0
  16. data/deps/system.rb +10 -0
  17. data/deps/templates/app.rb +68 -0
  18. data/deps/templates/external.rb +12 -0
  19. data/deps/templates/installer.rb +31 -0
  20. data/deps/templates/managed.rb +105 -0
  21. data/deps/templates/ppa.rb +24 -0
  22. data/deps/templates/src.rb +42 -0
  23. data/deps/templates/tmbundle.rb +15 -0
  24. data/lib/babushka.rb +28 -0
  25. data/lib/babushka/accepts_block_for.rb +72 -0
  26. data/lib/babushka/accepts_list_for.rb +49 -0
  27. data/lib/babushka/accepts_value_for.rb +24 -0
  28. data/lib/babushka/base.rb +78 -0
  29. data/lib/babushka/bug_reporter.rb +55 -0
  30. data/lib/babushka/cmdline.rb +133 -0
  31. data/lib/babushka/cmdline/handler.rb +41 -0
  32. data/lib/babushka/cmdline/helpers.rb +127 -0
  33. data/lib/babushka/cmdline/parser.rb +69 -0
  34. data/lib/babushka/colorizer.rb +59 -0
  35. data/lib/babushka/core_patches/array.rb +171 -0
  36. data/lib/babushka/core_patches/blank.rb +22 -0
  37. data/lib/babushka/core_patches/bytes.rb +52 -0
  38. data/lib/babushka/core_patches/hash.rb +107 -0
  39. data/lib/babushka/core_patches/hashish.rb +14 -0
  40. data/lib/babushka/core_patches/integer.rb +25 -0
  41. data/lib/babushka/core_patches/io.rb +8 -0
  42. data/lib/babushka/core_patches/numeric.rb +16 -0
  43. data/lib/babushka/core_patches/object.rb +27 -0
  44. data/lib/babushka/core_patches/string.rb +116 -0
  45. data/lib/babushka/core_patches/symbol.rb +12 -0
  46. data/lib/babushka/core_patches/try.rb +15 -0
  47. data/lib/babushka/core_patches/uri.rb +24 -0
  48. data/lib/babushka/dep.rb +470 -0
  49. data/lib/babushka/dep_context.rb +18 -0
  50. data/lib/babushka/dep_definer.rb +115 -0
  51. data/lib/babushka/dep_pool.rb +49 -0
  52. data/lib/babushka/dep_runner.rb +85 -0
  53. data/lib/babushka/dsl.rb +26 -0
  54. data/lib/babushka/git_repo.rb +185 -0
  55. data/lib/babushka/helpers/git_helpers.rb +32 -0
  56. data/lib/babushka/helpers/log_helpers.rb +176 -0
  57. data/lib/babushka/helpers/path_helpers.rb +34 -0
  58. data/lib/babushka/helpers/run_helpers.rb +145 -0
  59. data/lib/babushka/helpers/shell_helpers.rb +229 -0
  60. data/lib/babushka/helpers/suggest_helpers.rb +16 -0
  61. data/lib/babushka/helpers/uri_helpers.rb +36 -0
  62. data/lib/babushka/ip.rb +160 -0
  63. data/lib/babushka/lambda_chooser.rb +40 -0
  64. data/lib/babushka/levenshtein.rb +125 -0
  65. data/lib/babushka/meta_dep.rb +65 -0
  66. data/lib/babushka/meta_dep_context.rb +15 -0
  67. data/lib/babushka/parameter.rb +143 -0
  68. data/lib/babushka/pkg_helper.rb +81 -0
  69. data/lib/babushka/pkg_helpers/apt_helper.rb +61 -0
  70. data/lib/babushka/pkg_helpers/base_helper.rb +19 -0
  71. data/lib/babushka/pkg_helpers/binpkgsrc_helper.rb +48 -0
  72. data/lib/babushka/pkg_helpers/binports_helper.rb +34 -0
  73. data/lib/babushka/pkg_helpers/brew_helper.rb +110 -0
  74. data/lib/babushka/pkg_helpers/gem_helper.rb +120 -0
  75. data/lib/babushka/pkg_helpers/macports_helper.rb +22 -0
  76. data/lib/babushka/pkg_helpers/npm_helper.rb +45 -0
  77. data/lib/babushka/pkg_helpers/pacman_helper.rb +27 -0
  78. data/lib/babushka/pkg_helpers/pip_helper.rb +45 -0
  79. data/lib/babushka/pkg_helpers/src_helper.rb +16 -0
  80. data/lib/babushka/pkg_helpers/yum_helper.rb +25 -0
  81. data/lib/babushka/popen.rb +40 -0
  82. data/lib/babushka/prompt.rb +176 -0
  83. data/lib/babushka/renderable.rb +67 -0
  84. data/lib/babushka/resource.rb +215 -0
  85. data/lib/babushka/run_reporter.rb +60 -0
  86. data/lib/babushka/shell.rb +108 -0
  87. data/lib/babushka/source.rb +216 -0
  88. data/lib/babushka/source_pool.rb +146 -0
  89. data/lib/babushka/system_definitions.rb +97 -0
  90. data/lib/babushka/system_profile.rb +210 -0
  91. data/lib/babushka/task.rb +142 -0
  92. data/lib/babushka/vars.rb +108 -0
  93. data/lib/babushka/version_of.rb +65 -0
  94. data/lib/babushka/version_str.rb +57 -0
  95. data/lib/babushka/xml_string.rb +28 -0
  96. data/lib/components.rb +82 -0
  97. data/lib/fancypath/fancypath.rb +200 -0
  98. data/lib/inkan/inkan.rb +76 -0
  99. data/spec/acceptance/acceptance.rb +43 -0
  100. data/spec/acceptance_helper.rb +113 -0
  101. data/spec/archives/Blah.app.zip +0 -0
  102. data/spec/archives/archive.tar +0 -0
  103. data/spec/archives/archive.tar.bz2 +0 -0
  104. data/spec/archives/archive.tar.gz +0 -0
  105. data/spec/archives/archive.tbz2 +0 -0
  106. data/spec/archives/archive.tgz +0 -0
  107. data/spec/archives/archive.zip +0 -0
  108. data/spec/archives/content.txt +5 -0
  109. data/spec/archives/invalid_archive +5 -0
  110. data/spec/archives/nested archive/content.txt +5 -0
  111. data/spec/archives/nested_archive.tar +0 -0
  112. data/spec/archives/really_a_gzip.zip +0 -0
  113. data/spec/archives/test-0.3.1.tgz +0 -0
  114. data/spec/archives/tgz_archive +0 -0
  115. data/spec/archives/zip_without_extension +0 -0
  116. data/spec/babushka/accepts_for_spec.rb +174 -0
  117. data/spec/babushka/accepts_for_support.rb +72 -0
  118. data/spec/babushka/cmdline/console_spec.rb +11 -0
  119. data/spec/babushka/cmdline/help_spec.rb +61 -0
  120. data/spec/babushka/cmdline/version_spec.rb +10 -0
  121. data/spec/babushka/core_patches_spec.rb +171 -0
  122. data/spec/babushka/dep_context_spec.rb +58 -0
  123. data/spec/babushka/dep_definer_spec.rb +152 -0
  124. data/spec/babushka/dep_definer_support.rb +36 -0
  125. data/spec/babushka/dep_spec.rb +567 -0
  126. data/spec/babushka/dep_support.rb +29 -0
  127. data/spec/babushka/deps_spec.rb +113 -0
  128. data/spec/babushka/gem_helper_spec.rb +90 -0
  129. data/spec/babushka/git_repo_spec.rb +396 -0
  130. data/spec/babushka/ip_spec.rb +131 -0
  131. data/spec/babushka/lambda_chooser_spec.rb +115 -0
  132. data/spec/babushka/meta_dep_definer_spec.rb +127 -0
  133. data/spec/babushka/meta_dep_wrapper_spec.rb +32 -0
  134. data/spec/babushka/parameter_spec.rb +135 -0
  135. data/spec/babushka/path_helpers_spec.rb +102 -0
  136. data/spec/babushka/prompt_spec.rb +188 -0
  137. data/spec/babushka/renderable_spec.rb +100 -0
  138. data/spec/babushka/resource_spec.rb +141 -0
  139. data/spec/babushka/run_helpers_spec.rb +26 -0
  140. data/spec/babushka/shell_helpers_spec.rb +244 -0
  141. data/spec/babushka/shell_spec.rb +19 -0
  142. data/spec/babushka/source_pool_spec.rb +320 -0
  143. data/spec/babushka/source_pool_support.rb +31 -0
  144. data/spec/babushka/source_spec.rb +382 -0
  145. data/spec/babushka/source_support.rb +17 -0
  146. data/spec/babushka/system_profile_spec.rb +61 -0
  147. data/spec/babushka/task_spec.rb +141 -0
  148. data/spec/babushka/uri_spec.rb +13 -0
  149. data/spec/babushka/vars_spec.rb +59 -0
  150. data/spec/babushka/version_of_spec.rb +110 -0
  151. data/spec/babushka/version_str_spec.rb +130 -0
  152. data/spec/babushka/version_str_support.rb +37 -0
  153. data/spec/babushka/xml_string_spec.rb +98 -0
  154. data/spec/deps/bad/broken.rb +7 -0
  155. data/spec/deps/bad/working.rb +3 -0
  156. data/spec/deps/good/meta.rb +14 -0
  157. data/spec/deps/good/test.rb +11 -0
  158. data/spec/deps/outer/deps.rb +19 -0
  159. data/spec/deps/outer/more deps.rb +11 -0
  160. data/spec/deps/params/params.rb +10 -0
  161. data/spec/fancypath/fancypath_spec.rb +272 -0
  162. data/spec/fancypath_support.rb +10 -0
  163. data/spec/inkan/inkan_spec.rb +217 -0
  164. data/spec/renderable/different_example.conf.erb +4 -0
  165. data/spec/renderable/example.conf.erb +3 -0
  166. data/spec/renderable/example.sh +6 -0
  167. data/spec/renderable/with_binding.conf.erb +4 -0
  168. data/spec/renderable/xml_example.conf.erb +8 -0
  169. data/spec/repos/remote.git.tgz +0 -0
  170. data/spec/spec_helper.rb +87 -0
  171. metadata +238 -0
@@ -0,0 +1,41 @@
1
+ require 'abbrev'
2
+
3
+ module Babushka
4
+ module Cmdline
5
+
6
+ def handle name, description, &blk
7
+ Handler.add name, description, blk
8
+ end
9
+ module_function :handle
10
+
11
+ class Handler
12
+ def self.add name, description, opt_definer
13
+ Handler.new(name, description, opt_definer).tap {|handler|
14
+ (@handlers ||= []).push handler
15
+ }
16
+ end
17
+
18
+ def self.all
19
+ @handlers.reject {|h| h.name == 'global' }
20
+ end
21
+
22
+ def self.abbrev
23
+ all.map(&:name).abbrev
24
+ end
25
+
26
+ def self.for name
27
+ @handlers.detect {|h| h.name == name }
28
+ end
29
+
30
+ attr_reader :name, :description, :opt_definer, :handler
31
+
32
+ def initialize name, description, opt_definer
33
+ @name, @description, @opt_definer = name, description, (opt_definer || L{})
34
+ end
35
+
36
+ def run &handler
37
+ @handler = handler
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,127 @@
1
+ # coding: utf-8
2
+
3
+ module Babushka
4
+ module Cmdline
5
+ module_function
6
+
7
+ def fail_with message
8
+ log message if message.is_a? String
9
+ exit 1
10
+ end
11
+
12
+ module Helpers
13
+ extend LogHelpers
14
+ module_function
15
+
16
+ def print_version opts = {}
17
+ if opts[:full]
18
+ log "Babushka v#{VERSION}, (c) 2011 Ben Hoskings <ben@hoskings.net>"
19
+ else
20
+ log VERSION
21
+ end
22
+ end
23
+
24
+ def print_usage
25
+ log "\nThe gist:"
26
+ log " #{Base.program_name} <command> [options]"
27
+ log "\nAlso:"
28
+ log " #{Base.program_name} help <command> # Print command-specific usage info"
29
+ log " #{Base.program_name} <dep name> # A shortcut for 'babushka meet <dep name>'"
30
+ log " #{Base.program_name} babushka # Update babushka itself (what babushka.me/up does)"
31
+ end
32
+
33
+ def print_handlers
34
+ log "\nCommands:"
35
+ Handler.all.each {|handler|
36
+ log " #{handler.name.ljust(10)} #{handler.description}"
37
+ }
38
+ end
39
+
40
+ def print_examples
41
+ log "\nExamples:"
42
+ log " # Inspect the 'system' dep (and all its sub-deps) without touching the system.".colorize('grey')
43
+ log " #{Base.program_name} system --dry-run"
44
+ log "\n"
45
+ log " # Meet the 'fish' dep (i.e. install fish and all its dependencies).".colorize('grey')
46
+ log " #{Base.program_name} fish"
47
+ log "\n"
48
+ log " # Meet the 'user setup' dep, printing lots of debugging (including realtime".colorize('grey')
49
+ log " # shell command output).".colorize('grey')
50
+ log " #{Base.program_name} 'user setup' --debug"
51
+ end
52
+
53
+ def print_notes
54
+ log "\nCommands can be abbrev'ed, as long as they remain unique."
55
+ log " e.g. '#{Base.program_name} l' is short for '#{Base.program_name} list'."
56
+ end
57
+
58
+ def search_results_for q
59
+ YAML.load(search_webservice_for(q).body).sort_by {|i|
60
+ -i[:runs_this_week]
61
+ }.map {|i|
62
+ [
63
+ i[:name],
64
+ i[:source_uri],
65
+ ((i[:runs_this_week] && i[:runs_this_week] > 0) ? "#{i[:runs_this_week]} this week" : "#{i[:total_runs]} ever"),
66
+ ((i[:runs_this_week] && i[:runs_this_week] > 0) ? "#{(i[:success_rate_this_week] * 100).round}%" : ((i[:total_runs] && i[:total_runs] > 0) ? "#{(i[:total_success_rate] * 100).round}%" : '')),
67
+ (i[:source_uri][github_autosource_regex] ? "#{Base.program_name} #{$1}:#{"'" if i[:name][/\s/]}#{i[:name]}#{"'" if i[:name][/\s/]}" : '✣')
68
+ ]
69
+ }
70
+ end
71
+
72
+ def print_search_results search_term, results
73
+ log "The webservice knows about #{results.length} dep#{'s' unless results.length == 1} that match#{'es' if results.length == 1} '#{search_term}':"
74
+ log ""
75
+ Logging.log_table(
76
+ ['Name', 'Source', 'Runs', ' ✓', 'Command'],
77
+ results
78
+ )
79
+ if (custom_sources = results.select {|r| r[1][github_autosource_regex].nil? }.length) > 0
80
+ log ""
81
+ log "✣ #{custom_sources == 1 ? 'This source has a custom URI' : 'These sources have custom URIs'}, so babushka can't discover #{custom_sources == 1 ? 'it' : 'them'} automatically."
82
+ log " You can run #{custom_sources == 1 ? 'its' : 'their'} deps in the same way, though, once you add #{custom_sources == 1 ? 'it' : 'them'} manually:"
83
+ log " $ #{Base.program_name} sources -a <alias> <uri>"
84
+ log " $ #{Base.program_name} <alias>:<dep>"
85
+ end
86
+ end
87
+
88
+ def github_autosource_regex
89
+ /^git\:\/\/github\.com\/(.*)\/babushka-deps(\.git)?/
90
+ end
91
+
92
+ def search_webservice_for q
93
+ Net::HTTP.start('babushka.me') {|http|
94
+ http.get URI.escape("/deps/search.yaml/#{q}")
95
+ }
96
+ end
97
+
98
+ def generate_list_for to_list, filter_str
99
+ context = to_list == :deps ? Base.program_name : ':template =>'
100
+ match_str = filter_str.try(:downcase)
101
+ Base.sources.all_present.each {|source|
102
+ source.load!
103
+ }.map {|source|
104
+ [source, source.send(to_list).items]
105
+ }.map {|(source,items)|
106
+ if match_str.nil? || source.name.downcase[match_str]
107
+ [source, items]
108
+ else
109
+ [source, items.select {|item| item.name.downcase[match_str] }]
110
+ end
111
+ }.select {|(_,items)|
112
+ !items.empty?
113
+ }.sort_by {|(source,_)|
114
+ source.name
115
+ }.each {|(source,items)|
116
+ indent = (items.map {|item| "#{source.name}:#{item.name}".length }.max || 0) + 3
117
+ log ""
118
+ log "# #{source.name} (#{source.type})#{" - #{source.uri}" unless source.implicit?}"
119
+ log "# #{items.length} #{to_list.to_s.chomp(items.length == 1 ? 's' : '')}#{" matching '#{filter_str}'" unless filter_str.nil?}:"
120
+ items.each {|dep|
121
+ log "#{context} #{"'#{source.name}:#{dep.name}'".ljust(indent)}"
122
+ }
123
+ }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,69 @@
1
+ require 'optparse'
2
+
3
+ module Babushka
4
+ module Cmdline
5
+ class Parser
6
+ include LogHelpers
7
+
8
+ attr_reader :verb, :argv, :opts
9
+
10
+ def self.for argv
11
+ if argv.empty? || (%w[-h --help] & argv).any?
12
+ new 'help', argv
13
+ elsif !Handler.abbrev.has_key?(argv.first.sub(/^--/, ''))
14
+ new 'meet', argv, :implicit_verb => true
15
+ else
16
+ new Handler.abbrev[argv.shift.sub(/^--/, '')], argv
17
+ end
18
+ end
19
+
20
+ def initialize verb, argv, parse_opts = {}
21
+ @verb, @argv, @opts, @implicit_verb = verb, argv, default_opts, parse_opts[:implicit_verb]
22
+ parse(&Handler.for('global').opt_definer)
23
+ parse(&Handler.for(verb).opt_definer)
24
+ end
25
+
26
+ def run
27
+ parser.parse! argv
28
+ Handler.for(verb).handler.call self
29
+ rescue OptionParser::ParseError => e
30
+ log_error "The #{e.args.first} option #{error_reason(e)}. #{hint}"
31
+ end
32
+
33
+ def print_usage
34
+ log parser.to_s.sub(/^Usage:.*$/, '')
35
+ end
36
+
37
+ def parse &blk
38
+ instance_eval(&blk)
39
+ end
40
+
41
+ private
42
+
43
+ def default_opts
44
+ {
45
+ :"[no_]color" => $stdout.tty?
46
+ }
47
+ end
48
+
49
+ def hint
50
+ "`babushka#{" #{verb}" unless @implicit_verb} --help` for more info."
51
+ end
52
+
53
+ def error_reason e
54
+ {
55
+ OptionParser::MissingArgument => "requires an argument"
56
+ }[e.class] || "isn't valid"
57
+ end
58
+
59
+ def opt *args, &block
60
+ opt_name = args.collapse(/^--/).first.gsub(/\s.*$/, '').gsub('-', '_').to_sym
61
+ parser.on(*args) {|arg| opts[opt_name] = arg }
62
+ end
63
+
64
+ def parser
65
+ @parser ||= OptionParser.new
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,59 @@
1
+ class String
2
+
3
+ private
4
+
5
+ class Colorizer
6
+ include Singleton
7
+
8
+ HomeOffset = 29
9
+ LightOffset = 60
10
+ BGOffset = 10
11
+ LightRegex = /^light_/
12
+ ColorRegex = /^(light_)?none|gr[ae]y|red|green|yellow|blue|pink|cyan|white$/
13
+ CtrlRegex = /^bold|underlined?|blink(ing)?|reversed?$/
14
+ ColorOffsets = {
15
+ 'none' => 0,
16
+ 'gray' => 61, 'grey' => 61,
17
+ 'red' => 2,
18
+ 'green' => 3,
19
+ 'yellow' => 4,
20
+ 'blue' => 5,
21
+ 'pink' => 6,
22
+ 'cyan' => 7,
23
+ 'white' => 8
24
+ }
25
+ CtrlOffsets = {
26
+ 'bold' => 1,
27
+ 'underline' => 4, 'underlined' => 4,
28
+ 'blink' => 5, 'blinking' => 5,
29
+ 'reverse' => 7, 'reversed' => 7
30
+ }
31
+
32
+ def colorize text, description
33
+ return text if Babushka::Base.task.opts[:"[no_]color"] == false
34
+
35
+ terms = " #{description} ".gsub(' light ', ' light_').gsub(' on ', ' on_').strip.split(/\s+/)
36
+ bg = terms.detect {|i| /on_#{ColorRegex}/ =~ i }
37
+ fg = terms.detect {|i| ColorRegex =~ i }
38
+ ctrl = terms.detect {|i| CtrlRegex =~ i }
39
+
40
+ "\e[#{"0;#{fg_for(fg)};#{bg_for(bg) || ctrl_for(ctrl)}"}m#{text}\e[0m"
41
+ end
42
+
43
+ def fg_for name
44
+ light = name.gsub!(LightRegex, '') unless name.nil?
45
+ (ColorOffsets[name] || 0) + HomeOffset + (light ? LightOffset : 0)
46
+ end
47
+
48
+ def bg_for name
49
+ # There's a hole in the table on bg=none, so we use BGOffset to the left
50
+ offset = fg_for((name || '').sub(/^on_/, ''))
51
+ offset + BGOffset unless offset == HomeOffset
52
+ end
53
+
54
+ def ctrl_for name
55
+ CtrlOffsets[name] || HomeOffset
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,171 @@
1
+ class Array
2
+ # Returns true iff +other+ appears exactly at the start of +self+.
3
+ def starts_with? first, *rest
4
+ other = first.is_a?(Array) ? first : [first].concat(rest)
5
+ self[0, other.length] == other
6
+ end
7
+
8
+ # Returns true iff +other+ appears exactly at the end of +self+.
9
+ def ends_with? first, *rest
10
+ other = first.is_a?(Array) ? first : [first].concat(rest)
11
+ self[-other.length, other.length] == other
12
+ end
13
+
14
+ # Like #detect, but return the result of the block instead of the element.
15
+ def pick &block
16
+ value = nil
17
+ detect {|i| value = yield(i) }
18
+ value
19
+ end
20
+
21
+ # This is defined separately, and then aliased into place if required, so we
22
+ # can run specs against it no matter which ruby we're running against.
23
+ def local_group_by &block
24
+ inject({}) {|hsh,i|
25
+ (hsh[yield(i)] ||= []).push i
26
+ hsh
27
+ }
28
+ end
29
+ alias_method :group_by, :local_group_by unless [].respond_to?(:group_by)
30
+
31
+ # Return two arrays, the first being the portion of this array preceding the
32
+ # first element for which the block returs true, and the second being the
33
+ # remainder (or +nil+ if the block didn't return true for any elements).
34
+ def cut &block
35
+ if (cut_at = index {|i| yield i }).nil?
36
+ [self, nil]
37
+ else
38
+ [self[0...cut_at], self[cut_at..-1]]
39
+ end
40
+ end
41
+ # Return two arrays in the same manner as +cut+, but check for element
42
+ # equality against +value+ to find the point at which to cut the array.
43
+ def cut_at value
44
+ cut {|i| i == value }
45
+ end
46
+ # Return a new array containing every element from this array for which
47
+ # the block returns true.
48
+ def extract &block
49
+ dup.extract!(&block)
50
+ end
51
+ # Like +extract+, but remove the extracted values in-place before
52
+ # returning them.
53
+ def extract! &block
54
+ dup.inject [] do |extracted,i|
55
+ extracted << delete(i) if yield i
56
+ extracted
57
+ end
58
+ end
59
+ # Return a new array containing all the elements from this array that
60
+ # are neither +#nil?+ nor +#blank?+.
61
+ def squash
62
+ dup.squash!
63
+ end
64
+ # Like +squash+, but remove the +#nil?+ and +#blank?+ entries in-place.
65
+ def squash!
66
+ delete_if(&:blank?)
67
+ end
68
+
69
+ # Return a new array containing the elements that match +pattern+, with
70
+ # +pattern+ removed (or replaced via Array#sub, if +replacement+ is
71
+ # supplied).
72
+ #
73
+ # This is useful for selecting items from a list based on some label,
74
+ # removing the label at the same time. A good example is finding the current
75
+ # git branch. Given this repository:
76
+ # $ git branch
77
+ # master
78
+ # * next
79
+ # topic
80
+ # You can use +#collapse+ to retrieve the current branch like this:
81
+ # shell('git branch').split("\n").collapse(/\* /) #=> ["next"]
82
+ def collapse pattern, replacement = ''
83
+ grep(pattern).map {|i| i.sub pattern, replacement }
84
+ end
85
+
86
+ # Return a new array by converting each element in this array to a VersionOf.
87
+ def versions
88
+ map {|i| Babushka::VersionOf::Helpers.VersionOf i }
89
+ end
90
+
91
+ # Return a string describing this array as an English list. The final two
92
+ # elements are separated with 'and', and all the other elements are separated
93
+ # with commas.
94
+ #
95
+ # %w[John Paul Ringo George].to_list #=> "John, Paul, Ringo and George"
96
+ #
97
+ # A custom conjugation can be specified by passing +:conj+; if present, it
98
+ # will be used instead of 'and'.
99
+ #
100
+ # %[rain hail shine].to_list(:conj => 'or') #=> "rain, hail or shine"
101
+ #
102
+ # To add an oxford comma before the conjugation, pass +:oxford => true+.
103
+ #
104
+ # %w[hook line sinker].to_list(:oxford => true) #=> "hook, line, and sinker"
105
+ #
106
+ # If +:suffix+ is set, it will be appended along with the correct linking verb,
107
+ # i.e. 'is' for single-item lists and 'are' otherwise.
108
+ #
109
+ # %w[coffee].to_list(:suffix => 'great') => "coffee is great"
110
+ # %w[Cîroc Żubrówka].to_list(:suffix => 'vodkas') #=> "Cîroc and Żubrówka are vodkas"
111
+ #
112
+ # If +:limit+ is set, only the first +:limit+ items will be included in the
113
+ # output. If any elements were ommitted as a result, the suffix 'et al' will
114
+ # be appended to indicate there are missing elements.
115
+ #
116
+ # %w[latte espresso ristretto].to_list(:suffix => 'coffees', :limit => 2) #=> "latte, espresso et al are coffees"
117
+ #
118
+ # If +:noun+ is set in addition to +:limit+, it will be used to describe the
119
+ # length of the list after 'et al' if any items were ommitted as a result of
120
+ # the +:limit+ setting.
121
+ #
122
+ # %w[latte espresso ristretto].to_list(:limit => 2, :noun => 'coffees') #=> "latte, espresso et al - 3 coffees"
123
+ def to_list(opts = {})
124
+ items = map(&:to_s)
125
+ if opts[:limit].nil? || (length <= opts[:limit])
126
+ [
127
+ items[0..-2].squash.join(', '),
128
+ items.last
129
+ ].squash.join("#{',' if opts[:oxford]} #{opts[:conj] || 'and'} ")
130
+ else
131
+ items[0..(opts[:limit] - 1)].squash.join(', ') + ' et al' + (opts[:noun].nil? ? '' : " - #{length} #{opts[:noun]}")
132
+ end +
133
+ (opts[:suffix] ? " #{length > 1 ? 'are' : 'is'} #{opts[:suffix].strip}" : '')
134
+ end
135
+
136
+ # If the final element of the array is a +Hash+, it's removed from this array
137
+ # and returned. Otherwise, an empty hash is returned.
138
+ def extract_options!
139
+ last.is_a?(::Hash) ? pop : {}
140
+ end
141
+
142
+ # As above, without modifying the receiving object.
143
+ def extract_options
144
+ dup.extract_options!
145
+ end
146
+
147
+ # Return a new array containing the terms from this array that were
148
+ # determined to be 'similar to' +string+. A string is considered to
149
+ # be similar to another if its Levenshtein distance is less than
150
+ # either the string's length minus one, or one fifth is length plus
151
+ # two, whichever is less.
152
+ #
153
+ # word length 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 …
154
+ # typos allowed 0 0 1 2 3 3 3 3 3 4 4 4 4 4 5 …
155
+ #
156
+ # This means that:
157
+ # - a little over one fifth of strings longer than 4 characters can be misspelt;
158
+ # - strings 3 or 4 characters long can have 1 or 2 misspelt characters respectively;
159
+ # - strings 1 or 2 characters long must be spelt correctly.
160
+ def similar_to string
161
+ map {|term|
162
+ [term, Babushka::Levenshtein.distance(term, string)]
163
+ }.select {|(i, similarity)|
164
+ similarity <= [i.length - 2, (i.length / 5) + 2].min
165
+ }.sort_by {|(_, similarity)|
166
+ similarity
167
+ }.map {|(i, _)|
168
+ i
169
+ }
170
+ end
171
+ end