hack_tree 0.1.0

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 (38) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +74 -0
  6. data/Rakefile +1 -0
  7. data/hack_tree.gemspec +22 -0
  8. data/hacks/hack_tree/reload.rb +18 -0
  9. data/hacks/ls.rb +74 -0
  10. data/lib/generators/hack_tree/USAGE +7 -0
  11. data/lib/generators/hack_tree/hack_tree_generator.rb +9 -0
  12. data/lib/generators/hack_tree/templates/INSTALL +20 -0
  13. data/lib/generators/hack_tree/templates/hack_tree.rb +4 -0
  14. data/lib/generators/hack_tree/templates/hello.rb +16 -0
  15. data/lib/hack_tree/action_context.rb +125 -0
  16. data/lib/hack_tree/config.rb +27 -0
  17. data/lib/hack_tree/dsl_context.rb +95 -0
  18. data/lib/hack_tree/instance.rb +153 -0
  19. data/lib/hack_tree/node/base.rb +34 -0
  20. data/lib/hack_tree/node/group.rb +8 -0
  21. data/lib/hack_tree/node/hack.rb +10 -0
  22. data/lib/hack_tree/node.rb +13 -0
  23. data/lib/hack_tree/parser/base.rb +26 -0
  24. data/lib/hack_tree/parser/desc.rb +66 -0
  25. data/lib/hack_tree/tools.rb +26 -0
  26. data/lib/hack_tree.rb +233 -0
  27. data/spec/lib/hack_tree/parser/desc_spec/000,brief.txt +1 -0
  28. data/spec/lib/hack_tree/parser/desc_spec/000,full.txt +3 -0
  29. data/spec/lib/hack_tree/parser/desc_spec/000.txt +5 -0
  30. data/spec/lib/hack_tree/parser/desc_spec/010,brief.txt +1 -0
  31. data/spec/lib/hack_tree/parser/desc_spec/010,full.txt +3 -0
  32. data/spec/lib/hack_tree/parser/desc_spec/010.txt +8 -0
  33. data/spec/lib/hack_tree/parser/desc_spec.rb +50 -0
  34. data/spec/lib/hack_tree/parser/spec_helper.rb +3 -0
  35. data/spec/lib/hack_tree/spec_helper.rb +3 -0
  36. data/spec/lib/spec_helper.rb +3 -0
  37. data/spec/spec_helper.rb +53 -0
  38. metadata +94 -0
@@ -0,0 +1,34 @@
1
+ module HackTree
2
+ module Node
3
+ class Base
4
+ # Brief 1-line description, if present.
5
+ attr_accessor :brief_desc
6
+
7
+ # Multi-line description, if present.
8
+ attr_accessor :full_desc
9
+
10
+ # Node name, Symbol.
11
+ attr_accessor :name
12
+
13
+ # Parent group or <tt>nil</tt>.
14
+ attr_accessor :parent
15
+
16
+ def initialize(attrs = {})
17
+ attrs.each {|k, v| send("#{k}=", v)}
18
+ end
19
+
20
+ # global_name # => "hello"
21
+ # global_name # => "rails.db.tables"
22
+ def global_name
23
+ pcs = []
24
+ cursor = self
25
+ begin
26
+ pcs << cursor.name
27
+ cursor = cursor.parent
28
+ end while cursor
29
+
30
+ pcs.reverse.join(".")
31
+ end
32
+ end # Base
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ require File.join(File.dirname(__FILE__), "base")
2
+
3
+ module HackTree
4
+ module Node
5
+ class Group < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ require File.join(File.dirname(__FILE__), "base")
2
+
3
+ module HackTree
4
+ module Node
5
+ class Hack < Base
6
+ # The actual code block to execute.
7
+ attr_accessor :block
8
+ end # Hack
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module HackTree
2
+ module Node
3
+ # Node (group/hack) regexp without delimiters.
4
+ NAME_REGEXP = /[a-zA-Z_]\w*/
5
+
6
+ # Node names which can't be used due to serious reasons.
7
+ FORBIDDEN_NAMES = [
8
+ :inspect,
9
+ :method_missing,
10
+ :to_s,
11
+ ]
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module HackTree
2
+ module Parser
3
+ # Base class for parsers. Parsers generally process text into collection(s).
4
+ class Base
5
+ def initialize(attrs = {})
6
+ attrs.each {|k, v| send("#{k}=", v)}
7
+ end
8
+
9
+ # Synonym of #process.
10
+ def [](content)
11
+ process(content)
12
+ end
13
+
14
+ def process(content)
15
+ # NOTE: In parser meaning "content" argument name looks more solid. For mapper "data" is more appropriate. Both are okay for their cases.
16
+ raise "Redefine `process` in your class (#{self.class})"
17
+ end
18
+
19
+ private
20
+
21
+ def require_attr(attr)
22
+ send(attr) or raise "`#{attr}` is not set"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,66 @@
1
+ require File.join(File.dirname(__FILE__), "base")
2
+
3
+ module HackTree
4
+ module Parser
5
+ # DSL <tt>desc</tt> parser.
6
+ class Desc < Base
7
+ # Parse description text, always return Array of 2 elements.
8
+ #
9
+ # process(content) # => [nil, nil]. Neither brief nor full description is present.
10
+ # process(content) # => ["...", nil]. Brief description is present, full isn't.
11
+ # process(content) # => ["...", "..."]. Both brief and full descriptions are present.
12
+ def process(content)
13
+ lines = content.lstrip.lines.to_a
14
+ return [nil, nil] if lines.empty?
15
+
16
+ # If we're here, `brief` is certainly present.
17
+ brief = lines.shift.rstrip
18
+
19
+ # Extract full lines with original indentation on the left.
20
+
21
+ indented_lines = []
22
+ gap = true # We're skipping the gap between the brief and the full.
23
+
24
+ lines.each do |line|
25
+ line = line.rstrip
26
+ next if gap and line.empty?
27
+
28
+ # First non-empty line, the gap is over.
29
+ gap = false
30
+
31
+ indented_lines << line
32
+ end
33
+
34
+ # Compute minimum indentation level. Empty lines don't count.
35
+ indent = indented_lines.reject(&:empty?).map do |s|
36
+ s.match(/\A(\s*)\S/)[1].size
37
+ end.min.to_i
38
+
39
+ # Apply indentation.
40
+ unindented_lines = indented_lines.map do |line|
41
+ line.empty?? line : line[indent..-1]
42
+ end
43
+
44
+ # Reject empty lines at the end.
45
+ final_lines = []
46
+ buf = []
47
+ unindented_lines.each do |line|
48
+ # Accumulate empty lines.
49
+ if line.empty?
50
+ buf << line
51
+ next
52
+ end
53
+
54
+ # Non-empty line, flush `buf` and start over.
55
+ final_lines += buf + [line]
56
+ buf = []
57
+ end
58
+
59
+ [
60
+ brief,
61
+ (final_lines.join("\n") if not final_lines.empty?),
62
+ ]
63
+ end
64
+ end
65
+ end # Parser
66
+ end
@@ -0,0 +1,26 @@
1
+ module HackTree
2
+ module Tools
3
+ # compute_name_align(["alfa", "bravo"], 8..16) # => 8
4
+ def self.compute_name_align(names, limit)
5
+ (v = eval(vn = "names")).is_a?(klass = Array) or raise ArgumentError, "`#{vn}` must be #{klass}, #{v.class} (#{v.inspect}) given"
6
+ (v = eval(vn = "limit")).is_a?(klass = Range) or raise ArgumentError, "`#{vn}` must be #{klass}, #{v.class} (#{v.inspect}) given"
7
+
8
+ computed = names.map(&:size).select {|n| n <= limit.max}.max.to_i
9
+
10
+ [limit.min, computed].max
11
+ end
12
+
13
+ # format_node_name(node) # => "hello"
14
+ # format_node_name(node, "yo") # => "yo"
15
+ def self.format_node_name(node, name = nil)
16
+ case node
17
+ when Node::Group
18
+ ::HackTree.conf.group_format
19
+ when Node::Hack
20
+ ::HackTree.conf.hack_format
21
+ else
22
+ raise ArgumentError, "Unknown node class #{node.class}, SE"
23
+ end % (name || node.name).to_s
24
+ end
25
+ end
26
+ end
data/lib/hack_tree.rb ADDED
@@ -0,0 +1,233 @@
1
+ [
2
+ "hack_tree/**/*.rb",
3
+ ].each do |fmask|
4
+ Dir[File.join(File.dirname(__FILE__), fmask)].each do |fn|
5
+ require fn
6
+ end
7
+ end
8
+
9
+ module HackTree
10
+ VERSION = "0.1.0"
11
+
12
+ # Standard hacks bundled with the gem, their global names.
13
+ STD_HACKS = [
14
+ "hack_tree.reload",
15
+ "ls",
16
+ ]
17
+
18
+ # Clear everything. See Instance#clear.
19
+ def self.clear
20
+ instance.clear
21
+ end
22
+
23
+ # Clear config. See Instance#clear_conf.
24
+ def self.clear_conf
25
+ instance.clear_conf
26
+ end
27
+
28
+ # Clear group/hack definitions ("nodes" in general). See Instance#clear_nodes.
29
+ def self.clear_nodes
30
+ instance.clear_nodes
31
+ end
32
+
33
+ # Get configuration object.
34
+ #
35
+ # See also:
36
+ #
37
+ # * HackTree::Config
38
+ # * Instance#conf
39
+ def self.conf
40
+ instance.conf
41
+ end
42
+
43
+ # Define hacks.
44
+ #
45
+ # HackTree.define do
46
+ # group :greeting do
47
+ # desc "Say hello"
48
+ # hack :hello do |*args|
49
+ # puts "Hello, %s!" % (args[0] || "world")
50
+ # end
51
+ # end
52
+ # end
53
+ def self.define(&block)
54
+ instance.define(&block)
55
+ end
56
+
57
+ # Enable HackTree globally.
58
+ #
59
+ # >> HackTree.enable
60
+ # Greetings.
61
+ # >> c
62
+ # hello # Say hello
63
+ # >> c.hello
64
+ # Hello, world!
65
+ #
66
+ # Options:
67
+ #
68
+ # :completion => T|F # Enable completion enhancement. Default is true.
69
+ # :with_std => [...] # Load only these standard hacks.
70
+ # :without_std => [...] # Load all but these standard hacks.
71
+ # :quiet => T|F # Be quiet. Default is false.
72
+ #
73
+ # Examples:
74
+ #
75
+ # TODO.
76
+ def self.enable(method_name = :c, options = {})
77
+ options = options.dup
78
+ o = {}
79
+
80
+ o[k = :completion] = (v = options.delete(k)).nil?? true : v
81
+ o[k = :with_std] = options.delete(k)
82
+ o[k = :without_std] = options.delete(k)
83
+ o[k = :quiet] = (v = options.delete(k)).nil?? false : v
84
+
85
+ raise ArgumentError, "Unknown option(s): #{options.inspect}" if not options.empty?
86
+
87
+ if o[:with_std] and o[:without_std]
88
+ # Exception is better than warning. It has a stack trace.
89
+ raise ArgumentError, "Options `:with_std` and `:without_std` are mutually exclusive"
90
+ end
91
+
92
+ @enabled_as = method_name
93
+
94
+ if not o[:quiet]
95
+ # Print the banner before everything. If there are warnings, we'll know they are related to us somehow.
96
+ ::Kernel.puts "Console hacks are available. Use `%s`, `%s.hack?`, `%s.hack [args]`" % ([@enabled_as]*3)
97
+ end
98
+
99
+ # NOTE: This can't be put into `Instance`, it's a name-based global.
100
+ eval <<-EOT
101
+ module ::Kernel
102
+ private
103
+
104
+ def #{method_name}
105
+ ::HackTree.instance.action
106
+ end
107
+ end
108
+ EOT
109
+
110
+ # Install completion enhancement.
111
+ if o[:completion]
112
+ old_proc = Readline.completion_proc
113
+
114
+ Readline.completion_proc = lambda do |input|
115
+ candidates = instance.completion_logic(input, :enabled_as => @enabled_as)
116
+
117
+ # NOTE: Block result.
118
+ if candidates.is_a? Array
119
+ candidates
120
+ elsif old_proc
121
+ # Pass control.
122
+ old_proc.call(input)
123
+ else
124
+ # Nothing we can do.
125
+ []
126
+ end
127
+ end # @completion_proc =
128
+ end # if o[:completion]
129
+
130
+ # Load standard hacks.
131
+
132
+ global_names = if (ar = o[:with_std])
133
+ # White list.
134
+ STD_HACKS & ar.map(&:to_s)
135
+ elsif (ar = o[:without_std])
136
+ # Black list.
137
+ STD_HACKS - ar.map(&:to_s)
138
+ else
139
+ # Default.
140
+ STD_HACKS
141
+ end
142
+
143
+ global_names.each do |global_name|
144
+ bn = global_name.gsub(".", "/") + ".rb"
145
+ fn = File.join(File.dirname(__FILE__), "../hacks", bn)
146
+ load fn
147
+ end
148
+
149
+ nil
150
+ end
151
+
152
+ def self.enabled_as
153
+ @enabled_as
154
+ end
155
+
156
+ def self.instance
157
+ @instance ||= Instance.new
158
+ end
159
+ end
160
+
161
+ #--------------------------------------- Junk
162
+
163
+ if false
164
+ # * Using array is a reliable way to ensure a newline after the banner.
165
+ ::Kernel.puts [
166
+ #"",
167
+ "Console hacks are available. Use `%s`, `%s.hack?`, `%s.hack [args]`" % ([@enabled_as]*3),
168
+ #"",
169
+ ]
170
+ end
171
+
172
+ if false
173
+ # Node (group/hack) regexp without delimiters.
174
+ NAME_REGEXP = /[a-zA-Z_]\w*/
175
+
176
+ # Node names which can't be used due to serious reasons.
177
+ FORBIDDEN_NAMES = [
178
+ :inspect,
179
+ :method_missing,
180
+ :to_s,
181
+ ]
182
+ end
183
+
184
+ if false
185
+ # Create the action object.
186
+ #
187
+ # module Kernel
188
+ # # Access our hacks via <tt>c</tt>.
189
+ # def c
190
+ # ::HackTree.action
191
+ # end
192
+ # end
193
+ #
194
+ # >> c
195
+ # hello # Say hello
196
+ # >> c.hello
197
+ # Hello, world!
198
+ #
199
+ # See also ::enable.
200
+ def self.action
201
+ ActionContext.new(@nodes)
202
+ end
203
+
204
+ # Clear self.
205
+ def self.clear
206
+ # Request re-initialization upon first use of any kind.
207
+ @is_initialized = false
208
+ end
209
+
210
+ # Access nodes (groups/hacks) created via the DSL.
211
+ def self.nodes
212
+ @nodes
213
+ end
214
+
215
+ # See #nodes.
216
+ def self.nodes=(ar)
217
+ @nodes = ar
218
+ end
219
+
220
+ # NOTE: We need this wrapper to create private singletons.
221
+ class << self
222
+ private
223
+
224
+ # On-the-fly initializer.
225
+ def _otf_init
226
+ return if @is_initialized
227
+
228
+ @is_initialized = true
229
+ @nodes = []
230
+ @dsl_root = DslContext.new(@nodes)
231
+ end
232
+ end # class << self
233
+ end
@@ -0,0 +1,3 @@
1
+ Full 1
2
+
3
+ Full 2
@@ -0,0 +1,5 @@
1
+ Brief
2
+
3
+ Full 1
4
+
5
+ Full 2
@@ -0,0 +1 @@
1
+ Краткое описание
@@ -0,0 +1,3 @@
1
+ Длинное описание 1
2
+
3
+ Длинное описание 2
@@ -0,0 +1,8 @@
1
+ Краткое описание
2
+
3
+
4
+ Длинное описание 1
5
+
6
+ Длинное описание 2
7
+
8
+
@@ -0,0 +1,50 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ describe HackTree::Parser::Desc do
4
+ before :each do
5
+ @parser = described_class.new
6
+ end
7
+
8
+ it "should generally work" do
9
+ sets = [
10
+ ["", [nil, nil]],
11
+ [" ", [nil, nil]],
12
+ [" \t\n \n ", [nil, nil]],
13
+ ["Brief", ["Brief", nil]],
14
+ [" Brief\t\t\n\n", ["Brief", nil]],
15
+ ["\n\n\nBrief\t\t\n\n", ["Brief", nil]],
16
+ ["Brief\nFull 1 \nFull 2", ["Brief", "Full 1\nFull 2"]],
17
+ ["Brief\nFull 1 \nFull 2\n\n ", ["Brief", "Full 1\nFull 2"]],
18
+ ["Brief\n\n\nFull 1\nFull 2", ["Brief", "Full 1\nFull 2"]],
19
+ ["Brief\n Full 1\n Full 2", ["Brief", "Full 1\nFull 2"]],
20
+ ["Brief\n Full 1\n Full 2", ["Brief", "Full 1\n Full 2"]],
21
+ ["Brief\n Full 1\n\n Full 2", ["Brief", "Full 1\n\n Full 2"]],
22
+ ["Brief\n Full 1\n \n Full 2", ["Brief", "Full 1\n\n Full 2"]],
23
+
24
+ # File-based tests for more complex cases.
25
+ [["000"], [["000,brief"], ["000,full"]]],
26
+ [["010"], [["010,brief"], ["010,full"]]],
27
+ ]
28
+
29
+ path = Pathname(__FILE__[0..-4])
30
+
31
+ sets.each do |input_spec, expected_spec|
32
+ input = if input_spec.is_a? Array
33
+ # Input is a file reference.
34
+ File.read(path + "#{input_spec[0]}.txt")
35
+ else
36
+ # Input is plain.
37
+ input_spec
38
+ end
39
+
40
+ expected = expected_spec.map do |spec|
41
+ # Same rule as for input.
42
+ spec.is_a?(Array) ? File.read(path + "#{spec[0]}.txt") : spec
43
+ end
44
+
45
+ print_on_failure("-- input_spec:#{input_spec.inspect}") do
46
+ @parser[input].should == expected
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), "../spec_helper")
2
+
3
+ # Custom stuff for this group.
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), "../spec_helper")
2
+
3
+ # Custom stuff for this group.
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), "../spec_helper")
2
+
3
+ # Custom stuff for this group.
@@ -0,0 +1,53 @@
1
+ require "pathname"
2
+
3
+ # Load stuff.
4
+ [
5
+ "lib/**/*.rb",
6
+ ].each do |fmask|
7
+ Dir["./#{fmask}"].each do |fn|
8
+ ##puts "-- req '#{fn}'"
9
+ require fn
10
+ end
11
+ end
12
+
13
+ # TODO: When this becomes a gem, use gem instead of direct copy.
14
+ module RSpec
15
+ module PrintOnFailure
16
+ module Helpers
17
+ # Output <tt>message</tt> before the failed tests in <tt>block</tt>. Useful when input and expected data
18
+ # are defined as collections.
19
+ #
20
+ # sets = [
21
+ # ["hello", "HELLO"],
22
+ # ["123", "456"],
23
+ # ]
24
+ #
25
+ # sets.each do |input, expected|
26
+ # print_on_failure("-- input:'#{input}'") do
27
+ # input.upcase.should == expected
28
+ # end
29
+ # end
30
+ def print_on_failure(message, &block)
31
+ begin
32
+ yield
33
+ rescue Exception
34
+ # Catch just everything, report and then re-run. The test may fail due to an exception, not necessarily unmatched expectation.
35
+ puts message
36
+ yield
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ begin
44
+ # 2.x.
45
+ RSpec.configure do |config|
46
+ config.include ::RSpec::PrintOnFailure::Helpers
47
+ end
48
+ rescue NameError
49
+ # 1.3.
50
+ Spec::Runner.configure do |config|
51
+ config.include ::RSpec::PrintOnFailure::Helpers
52
+ end
53
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hack_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Fortuna
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &71051990 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *71051990
25
+ description: HackTree lets you organize and share your console hacks in an effective
26
+ and uniform way. Blah-blah-blah.
27
+ email:
28
+ - alex.r@askit.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - .rspec
35
+ - Gemfile
36
+ - MIT-LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - hack_tree.gemspec
40
+ - hacks/hack_tree/reload.rb
41
+ - hacks/ls.rb
42
+ - lib/generators/hack_tree/USAGE
43
+ - lib/generators/hack_tree/hack_tree_generator.rb
44
+ - lib/generators/hack_tree/templates/INSTALL
45
+ - lib/generators/hack_tree/templates/hack_tree.rb
46
+ - lib/generators/hack_tree/templates/hello.rb
47
+ - lib/hack_tree.rb
48
+ - lib/hack_tree/action_context.rb
49
+ - lib/hack_tree/config.rb
50
+ - lib/hack_tree/dsl_context.rb
51
+ - lib/hack_tree/instance.rb
52
+ - lib/hack_tree/node.rb
53
+ - lib/hack_tree/node/base.rb
54
+ - lib/hack_tree/node/group.rb
55
+ - lib/hack_tree/node/hack.rb
56
+ - lib/hack_tree/parser/base.rb
57
+ - lib/hack_tree/parser/desc.rb
58
+ - lib/hack_tree/tools.rb
59
+ - spec/lib/hack_tree/parser/desc_spec.rb
60
+ - spec/lib/hack_tree/parser/desc_spec/000,brief.txt
61
+ - spec/lib/hack_tree/parser/desc_spec/000,full.txt
62
+ - spec/lib/hack_tree/parser/desc_spec/000.txt
63
+ - spec/lib/hack_tree/parser/desc_spec/010,brief.txt
64
+ - spec/lib/hack_tree/parser/desc_spec/010,full.txt
65
+ - spec/lib/hack_tree/parser/desc_spec/010.txt
66
+ - spec/lib/hack_tree/parser/spec_helper.rb
67
+ - spec/lib/hack_tree/spec_helper.rb
68
+ - spec/lib/spec_helper.rb
69
+ - spec/spec_helper.rb
70
+ homepage: http://github.com/dadooda/hack_tree
71
+ licenses: []
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.10
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Organize and share your console hacks
94
+ test_files: []