executable 1.2.1 → 1.3.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dd281be8d7d039a36b1628be53a86817d12d30f1d69258d1f5b0dfa84cc99ad4
4
+ data.tar.gz: 459d53385e0333dd991c4d93f1a2b06e14cf8af188f541d7a0eb50f13b265e45
5
+ SHA512:
6
+ metadata.gz: c519372eba2ab84e43c919859e52117fbd3d0715ded1721b99f0b471221630ebdd55b8a58485dc6544f3e557a43d71d2fea6e828179dab02a76fbc084388e4a3
7
+ data.tar.gz: b08c3be0ddf83ac87fb68e29404add4b847b95c6aaaa748bb40bbfd8e585c6a6b126fba8649750776d8d544a5c0600b1e1ee0216bad9990e7a9b4b974bcabeec
data/HISTORY.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # RELEASE HISTORY
2
2
 
3
+ ## 1.3.0 / 2026-04-05
4
+
5
+ Maintenance release. Modernized project tooling.
6
+
7
+ Changes:
8
+
9
+ * Replace custom Indexer system with standard gemspec.
10
+ * Replace Travis CI with GitHub Actions.
11
+ * Update Rakefile.
12
+ * Fix typo in version.rb module name (Exectuable -> Executable).
13
+ * Switch tests to minitest.
14
+ * Update URLs to HTTPS.
15
+ * Clean up obsolete files and .gitignore.
16
+
17
+
3
18
  ## 1.2.1 / 2012-12-19
4
19
 
5
20
  This release imporves the help output and manpage lookup
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- [Website](http://rubyworks.github.com/executable) |
2
- [Report Issue](http://github.com/rubyworks/executable/features) |
3
- [Source Code](http://github.com/rubyworks/executable)
4
- ( [![Build Status](https://secure.travis-ci.org/rubyworks/indexer.png)](http://travis-ci.org/rubyworks/indexer) )
1
+ # Executable
5
2
 
3
+ [Source Code](https://github.com/rubyworks/executable) |
4
+ [Report Issue](https://github.com/rubyworks/executable/issues)
6
5
 
7
- # Executable
6
+ [![Gem Version](https://img.shields.io/gem/v/executable.svg?style=flat)](https://rubygems.org/gems/executable)
7
+ [![Build Status](https://github.com/rubyworks/executable/actions/workflows/test.yml/badge.svg)](https://github.com/rubyworks/executable/actions/workflows/test.yml)
8
8
 
9
9
  Executable is to the commandline, what ActiveRecord is the database.
10
10
  You can think of Executable as a *COM*, a Command-line Object Mapper,
@@ -30,7 +30,7 @@ syntax. No special DSL is required.
30
30
  * Help doesn't handle aliases well (yet).
31
31
 
32
32
 
33
- ## Instructions
33
+ ## Overview
34
34
 
35
35
  CLIs can be built by using a Executable as a mixin, or by subclassing
36
36
  `Executable::Command`. Methods seemlessly handle command-line options.
@@ -56,11 +56,11 @@ For example, here is a simple "Hello, World!" commandline tool.
56
56
  end
57
57
 
58
58
  # Show this message.
59
- def help?
59
+ def help!
60
60
  cli.show_help
61
61
  exit
62
62
  end
63
- alias :h? :help?
63
+ alias :h! :help!
64
64
 
65
65
  # Say hello.
66
66
  def call(name)
@@ -122,8 +122,9 @@ to generate the manpages. What's particularly cool about Executable,
122
122
  is that once we have a manpage in the standard `man/` location in our project,
123
123
  the `#show_help` method will use it instead of the plain text.
124
124
 
125
- For a more detail example see [QED](demo.html)
126
- and [API](http://rubydoc.info/gems/executable/frames) documentation.
125
+ For a more detailed example see [QED](http://rubyworks.github.com/executable/demo.html),
126
+ [API](http://rubydoc.info/gems/executable/frames) documentation and, in particular,
127
+ the [Wiki](http://wiki.github.com/rubyworks/).
127
128
 
128
129
 
129
130
  ## Installation
@@ -134,26 +135,45 @@ Install with RubyGems in the usual fashion.
134
135
  $ gem install executable
135
136
  ```
136
137
 
137
- ## CONTRIBUTING
138
+ ## Contributing
139
+
140
+ Executable is a [Rubyworks](http://rubyworks.github.com) project. As such it largely
141
+ uses in-house tools for development.
142
+
143
+ ### Submitting Patches
144
+
145
+ If it is a very small change, just pasting it to an issue is fine. For anything more than
146
+ this please send us a traditional patch, but even better use Github pull requests.
147
+ Good contributions have the following:
148
+
149
+ * Well documented code following the conventions of the project.
150
+ * Clearly written tests with good test coverage written using the project's chosen test framework.
151
+ * Use of a git topic branch to keep the change set well isolated.
138
152
 
139
- Executable is a Rubyworks project. As such we largely use in house tools
140
- for development.
153
+ The more of these bullet points a pull request covers, the more likely and quickly it will
154
+ be accepted and merged.
141
155
 
142
156
  ### Testing
143
157
 
144
- QED and Microtest are being used for this project.
158
+ [QED](http://rubyworks.github.com/qed) and [Microtest](http://rubyworks.github.com/microtest)
159
+ are used for this project. To run the QED demos just run the `qed` command, probably with bundler,
160
+ so `bundle exec qed`. And to run the microtests you can use `rubytest test/`, again with bundler,
161
+ `bundle exec rubytest test/`.
145
162
 
146
163
  ### Getting In Touch
147
164
 
148
- * [#rubyworks](irc://irc.freenode.org/rubyworks)
165
+ For direct dialog we have an IRC channel, #rubyworks on freenode. But it's not always manned,
166
+ so a [mailing list](http://groups.google.com/groups/rubyworks-mailinglist) is also available.
167
+ Of course these days, the GitHub [issues page](http://github.com/rubyworks/executable) is
168
+ generally the place get in touch for anything specific to this project.
149
169
 
150
170
 
151
171
  ## Copyrights
152
172
 
153
173
  Executable is copyrighted open source software.
154
174
 
155
- Copyright (c) 2008 Rubyworks (BSD-2-Clause License)
175
+ Copyright (c) 2008 Rubyworks (BSD-2-Clause)
156
176
 
157
- It can be distributed and modified in accordance with the *BSD-2-Clause* license.
177
+ It can be distributed and modified in accordance with the **BSD-2-Clause** license.
158
178
 
159
179
  See LICENSE.txt for details.
@@ -0,0 +1,82 @@
1
+ module Executable
2
+
3
+ # Encpsulates command completion.
4
+ #
5
+ class Completion
6
+
7
+ #
8
+ # Setup new completion object.
9
+ #
10
+ def initialize(cli_class)
11
+ @cli_class = cli_class
12
+
13
+ @subcommands = nil
14
+ @options = nil
15
+ end
16
+
17
+ #
18
+ alias_method :inspect, :to_s
19
+
20
+ #
21
+ # The Executable subclass to which this help applies.
22
+ #
23
+ attr :cli_class
24
+
25
+ #
26
+ # List of subcommands converted to a printable string.
27
+ # But will return +nil+ if there are no subcommands.
28
+ #
29
+ # @return [String,NilClass] subcommand list text
30
+ #
31
+ def subcommands
32
+ @subcommands ||= @cli_class.subcommands.keys
33
+ end
34
+
35
+ #
36
+ def options
37
+ @options ||= (
38
+ method_list.map do |meth|
39
+ case meth.name
40
+ when /^(.*?)[\!\=]$/
41
+ name = meth.name.to_s.chomp('!').chomp('=')
42
+ mark = name.to_s.size == 1 ? '-' : '--'
43
+ mark + name
44
+ end
45
+ end.compact.sort
46
+ )
47
+ end
48
+
49
+ #
50
+ def to_s
51
+ (subcommands + options).join(' ')
52
+ end
53
+
54
+ #
55
+ def call(*args)
56
+ puts self
57
+ end
58
+
59
+ private
60
+
61
+ #
62
+ # Produce a list relavent methods.
63
+ #
64
+ def method_list
65
+ list = []
66
+ methods = []
67
+ stop_at = cli_class.ancestors.index(Executable::Command) ||
68
+ cli_class.ancestors.index(Executable) ||
69
+ -1
70
+ ancestors = cli_class.ancestors[0...stop_at]
71
+ ancestors.reverse_each do |a|
72
+ a.instance_methods(false).each do |m|
73
+ list << cli_class.instance_method(m)
74
+ end
75
+ end
76
+ list
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,102 @@
1
+ class UnboundMethod
2
+ if !method_defined?(:source_location)
3
+ if Proc.method_defined? :__file__ # /ree/
4
+ def source_location
5
+ [__file__, __line__] rescue nil
6
+ end
7
+ elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
8
+ require 'java'
9
+ def source_location
10
+ to_java.source_location(Thread.current.to_java.getContext())
11
+ end
12
+ end
13
+ end
14
+
15
+ #
16
+ def comment
17
+ Source.get_above_comment(*source_location)
18
+ end
19
+
20
+ # Source lookup.
21
+ #
22
+ module Source
23
+ extend self
24
+
25
+ # Read and cache file.
26
+ #
27
+ # @param file [String] filename, should be full path
28
+ #
29
+ # @return [Array] file content in array of lines
30
+ def read(file)
31
+ @read ||= {}
32
+ @read[file] ||= File.readlines(file)
33
+ end
34
+
35
+ # Get comment from file searching up from given line number.
36
+ #
37
+ # @param file [String] filename, should be full path
38
+ # @param line [Integer] line number in file
39
+ #
40
+ def get_above_comment(file, line)
41
+ get_above_comment_lines(file, line).join("\n").strip
42
+ end
43
+
44
+ # Get comment from file searching up from given line number.
45
+ #
46
+ # @param file [String] filename, should be full path
47
+ # @param line [Integer] line number in file
48
+ #
49
+ def get_above_comment_lines(file, line)
50
+ text = read(file)
51
+ index = line - 1
52
+ while index >= 0 && text[index] !~ /^\s*\#/
53
+ return [] if text[index] =~ /^\s*end/
54
+ index -= 1
55
+ end
56
+ rindex = index
57
+ while text[index] =~ /^\s*\#/
58
+ index -= 1
59
+ end
60
+ result = text[index..rindex]
61
+ result = result.map{ |s| s.strip }
62
+ result = result.reject{ |s| s[0,1] != '#' }
63
+ result = result.map{ |s| s.sub(/^#/,'').strip }
64
+ #result = result.reject{ |s| s == "" }
65
+ result
66
+ end
67
+
68
+ # Get comment from file searching down from given line number.
69
+ #
70
+ # @param file [String] filename, should be full path
71
+ # @param line [Integer] line number in file
72
+ #
73
+ def get_following_comment(file, line)
74
+ get_following_comment_lines(file, line).join("\n").strip
75
+ end
76
+
77
+ # Get comment from file searching down from given line number.
78
+ #
79
+ # @param file [String] filename, should be full path
80
+ # @param line [Integer] line number in file
81
+ #
82
+ def get_following_comment_lines(file, line)
83
+ text = read(file)
84
+ index = line || 0
85
+ while text[index] !~ /^\s*\#/
86
+ return nil if text[index] =~ /^\s*(class|module)/
87
+ index += 1
88
+ end
89
+ rindex = index
90
+ while text[rindex] =~ /^\s*\#/
91
+ rindex += 1
92
+ end
93
+ result = text[index..(rindex-2)]
94
+ result = result.map{ |s| s.strip }
95
+ result = result.reject{ |s| s[0,1] != '#' }
96
+ result = result.map{ |s| s.sub(/^#/,'').strip }
97
+ result.join("\n").strip
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -1,102 +1 @@
1
- class UnboundMethod
2
- if !method_defined?(:source_location)
3
- if Proc.method_defined? :__file__ # /ree/
4
- def source_location
5
- [__file__, __line__] rescue nil
6
- end
7
- elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
8
- require 'java'
9
- def source_location
10
- to_java.source_location(Thread.current.to_java.getContext())
11
- end
12
- end
13
- end
14
-
15
- #
16
- def comment
17
- Source.get_above_comment(*source_location)
18
- end
19
-
20
- # Source lookup.
21
- #
22
- module Source
23
- extend self
24
-
25
- # Read and cache file.
26
- #
27
- # @param file [String] filename, should be full path
28
- #
29
- # @return [Array] file content in array of lines
30
- def read(file)
31
- @read ||= {}
32
- @read[file] ||= File.readlines(file)
33
- end
34
-
35
- # Get comment from file searching up from given line number.
36
- #
37
- # @param file [String] filename, should be full path
38
- # @param line [Integer] line number in file
39
- #
40
- def get_above_comment(file, line)
41
- get_above_comment_lines(file, line).join("\n").strip
42
- end
43
-
44
- # Get comment from file searching up from given line number.
45
- #
46
- # @param file [String] filename, should be full path
47
- # @param line [Integer] line number in file
48
- #
49
- def get_above_comment_lines(file, line)
50
- text = read(file)
51
- index = line - 1
52
- while index >= 0 && text[index] !~ /^\s*\#/
53
- return [] if text[index] =~ /^\s*end/
54
- index -= 1
55
- end
56
- rindex = index
57
- while text[index] =~ /^\s*\#/
58
- index -= 1
59
- end
60
- result = text[index..rindex]
61
- result = result.map{ |s| s.strip }
62
- result = result.reject{ |s| s[0,1] != '#' }
63
- result = result.map{ |s| s.sub(/^#/,'').strip }
64
- #result = result.reject{ |s| s == "" }
65
- result
66
- end
67
-
68
- # Get comment from file searching down from given line number.
69
- #
70
- # @param file [String] filename, should be full path
71
- # @param line [Integer] line number in file
72
- #
73
- def get_following_comment(file, line)
74
- get_following_comment_lines(file, line).join("\n").strip
75
- end
76
-
77
- # Get comment from file searching down from given line number.
78
- #
79
- # @param file [String] filename, should be full path
80
- # @param line [Integer] line number in file
81
- #
82
- def get_following_comment_lines(file, line)
83
- text = read(file)
84
- index = line || 0
85
- while text[index] !~ /^\s*\#/
86
- return nil if text[index] =~ /^\s*(class|module)/
87
- index += 1
88
- end
89
- rindex = index
90
- while text[rindex] =~ /^\s*\#/
91
- rindex += 1
92
- end
93
- result = text[index..(rindex-2)]
94
- result = result.map{ |s| s.strip }
95
- result = result.reject{ |s| s[0,1] != '#' }
96
- result = result.map{ |s| s.sub(/^#/,'').strip }
97
- result.join("\n").strip
98
- end
99
-
100
- end
101
-
102
- end
1
+ require 'executable/core_ext/unbound_method'
@@ -3,7 +3,7 @@ module Executable
3
3
  #
4
4
  module Domain
5
5
 
6
- #
6
+ # TODO: Should this be in Help class?
7
7
  def usage_name
8
8
  list = []
9
9
  ancestors.each do |ancestor|
@@ -38,18 +38,24 @@ module Executable
38
38
  # @name
39
39
  # end
40
40
  #
41
+ # TODO: Currently there is an unfortunate issue with using
42
+ # this helper method. If does not correctly record the location
43
+ # the method is called, so default help message is wrong.
41
44
  #
42
45
  def attr_switch(name)
43
- attr_writer name
44
- module_eval %{
46
+ file, line = *caller[0].split(':')[0..1]
47
+ module_eval(<<-END, file, line.to_i)
48
+ def #{name}=(value)
49
+ @#{name}=(value)
50
+ end
45
51
  def #{name}?
46
52
  @#{name}
47
53
  end
48
- }
54
+ END
49
55
  end
50
56
 
51
57
  #
52
- #
58
+ # Alias a switch.
53
59
  #
54
60
  def alias_switch(name, origin)
55
61
  alias_method "#{name}=", "#{origin}="
@@ -57,7 +63,7 @@ module Executable
57
63
  end
58
64
 
59
65
  #
60
- #
66
+ # Alias an accessor.
61
67
  #
62
68
  def alias_accessor(name, origin)
63
69
  alias_method "#{name}=", "#{origin}="
@@ -72,7 +78,8 @@ module Executable
72
78
  end
73
79
 
74
80
  #
75
- # Returns `help.to_s`.
81
+ # Returns `help.to_s`. If you want to provide your own help
82
+ # text you can override this method in your command subclass.
76
83
  #
77
84
  def to_s
78
85
  cli.to_s
@@ -5,9 +5,6 @@ module Executable
5
5
  # Encpsulates command help for defining and displaying well formated help
6
6
  # output in plain text, markdown or via manpages if found.
7
7
  #
8
- # TODO: Currently doesn't hande aliases/shortcuts well and simply
9
- # lists them as separate entries.
10
- #
11
8
  # Creating text help in the fly is fine for personal projects, but
12
9
  # for production app, ideally you want to have man pages. You can
13
10
  # use the #markdown method to generate `.ronn` files and use the
@@ -30,7 +27,9 @@ module Executable
30
27
  }
31
28
  end
32
29
 
30
+ #
33
31
  # Setup new help object.
32
+ #
34
33
  def initialize(cli_class)
35
34
  @cli_class = cli_class
36
35
 
@@ -198,8 +197,8 @@ module Executable
198
197
  end
199
198
 
200
199
  if !options.empty?
201
- s << "OPTIONS\n" + options.map{ |max, opt|
202
- " %2s%-#{max}s %s" % [opt.mark, opt.usage, opt.description]
200
+ s << "OPTIONS\n" + options.map{ |max, opts, desc|
201
+ " %-#{max}s %s" % [opts.join(' '), desc]
203
202
  }.join("\n")
204
203
  end
205
204
 
@@ -240,8 +239,8 @@ module Executable
240
239
 
241
240
  if !options.empty?
242
241
  s << "## OPTIONS"
243
- s << options.map{ |max, opt|
244
- " * `#{opt.mark}%s`:\n %s" % [opt.usage, opt.description]
242
+ s << options.map{ |max, opts, desc|
243
+ " * `%s`:\n %s" % [opts.join(' '), desc]
245
244
  }.join("\n\n")
246
245
  end
247
246
 
@@ -301,10 +300,17 @@ module Executable
301
300
  end
302
301
  end
303
302
 
304
- max = option_list.map{ |opt| opt.usage.size }.max.to_i + 2
303
+ # if two options have the same description, they must aliases
304
+ aliased_options = option_list.group_by{ |opt| opt.description }
305
+
306
+ list = aliased_options.map do |desc, opts|
307
+ [opts.map{ |o| "%s%s" % [o.mark, o.usage] }, desc]
308
+ end
309
+
310
+ max = list.map{ |opts, desc| opts.join(' ').size }.max.to_i + 2
305
311
 
306
- option_list.map do |opt|
307
- [max, opt]
312
+ list.map do |opts, desc|
313
+ [max, opts, desc]
308
314
  end
309
315
  end
310
316
 
@@ -350,11 +356,11 @@ module Executable
350
356
  -1
351
357
  ancestors = cli_class.ancestors[0...stop_at]
352
358
  ancestors.reverse_each do |a|
353
- a.instance_methods(false).each do |m|
359
+ a.public_instance_methods(false).each do |m|
354
360
  list << cli_class.instance_method(m)
355
361
  end
356
362
  end
357
- list
363
+ list #.uniq
358
364
  end
359
365
 
360
366
  #
@@ -25,7 +25,12 @@ module Executable
25
25
 
26
26
  cmd, argv = parse_subcommand(argv)
27
27
  cli = cmd.new
28
- args = parse_arguments(cli, argv)
28
+
29
+ if argv.first == "_"
30
+ cli = Completion.new(cli.class)
31
+ else
32
+ args = parse_arguments(cli, argv)
33
+ end
29
34
 
30
35
  return cli, args
31
36
  end
@@ -141,8 +146,7 @@ module Executable
141
146
  end
142
147
  end
143
148
 
144
- # TODO: parse_flags needs some thought concerning character
145
- # spliting and arguments.
149
+ # TODO: parse_flags needs some thought concerning character spliting and arguments.
146
150
 
147
151
  #
148
152
  # Parse single-dash command-line option.
@@ -1,20 +1,3 @@
1
- module Exectuable
2
-
3
- #
4
- DIRECTORY = File.dirname(__FILE__)
5
-
6
- #
7
- def self.const_missing(name)
8
- index[name.to_s.downcase] || super(name)
9
- end
10
-
11
- #
12
- def self.index
13
- @index ||= (
14
- require 'yaml'
15
- YAML.load(File.new(DIRECTORY + '/../executable.yml'))
16
- )
17
- end
18
-
1
+ module Executable
2
+ VERSION = '1.3.0'
19
3
  end
20
-
data/lib/executable.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'executable/errors'
4
4
  require 'executable/parser'
5
5
  require 'executable/help'
6
+ require 'executable/completion'
6
7
  require 'executable/utils'
7
8
  require 'executable/domain'
8
9
  require 'executable/dispatch'
@@ -58,7 +59,7 @@ public
58
59
  end
59
60
 
60
61
  #
61
- # Access to underlying Help instance.
62
+ # Access to underlying cli instance.
62
63
  #
63
64
  def cli
64
65
  self.class.cli