ursm-ditz 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +15 -0
- data/INSTALL +20 -0
- data/LICENSE +674 -0
- data/Manifest.txt +40 -0
- data/PLUGINS.txt +140 -0
- data/README.txt +43 -27
- data/Rakefile +36 -3
- data/ReleaseNotes +6 -0
- data/bin/ditz +49 -63
- data/contrib/completion/ditz.bash +25 -9
- data/lib/ditz.rb +52 -7
- data/lib/ditz/file-storage.rb +54 -0
- data/lib/{hook.rb → ditz/hook.rb} +1 -1
- data/lib/{html.rb → ditz/html.rb} +42 -4
- data/lib/{lowline.rb → ditz/lowline.rb} +31 -11
- data/lib/{model-objects.rb → ditz/model-objects.rb} +53 -21
- data/lib/ditz/model.rb +321 -0
- data/lib/{operator.rb → ditz/operator.rb} +122 -67
- data/lib/ditz/plugins/git-sync.rb +83 -0
- data/lib/{plugins → ditz/plugins}/git.rb +57 -18
- data/lib/ditz/plugins/issue-claiming.rb +174 -0
- data/lib/ditz/plugins/issue-labeling.rb +161 -0
- data/lib/{util.rb → ditz/util.rb} +4 -0
- data/lib/{view.rb → ditz/view.rb} +0 -0
- data/lib/{views.rb → ditz/views.rb} +7 -4
- data/man/{ditz.1 → man1/ditz.1} +1 -1
- data/setup.rb +1585 -0
- data/share/ditz/blue-check.png +0 -0
- data/{lib → share/ditz}/component.rhtml +7 -5
- data/share/ditz/green-bar.png +0 -0
- data/share/ditz/green-check.png +0 -0
- data/share/ditz/index.rhtml +130 -0
- data/share/ditz/issue.rhtml +119 -0
- data/share/ditz/issue_table.rhtml +28 -0
- data/share/ditz/red-check.png +0 -0
- data/share/ditz/release.rhtml +98 -0
- data/share/ditz/style.css +226 -0
- data/share/ditz/unassigned.rhtml +23 -0
- data/share/ditz/yellow-bar.png +0 -0
- metadata +50 -28
- data/lib/index.rhtml +0 -113
- data/lib/issue.rhtml +0 -111
- data/lib/issue_table.rhtml +0 -33
- data/lib/model.rb +0 -208
- data/lib/plugins/issue-claiming.rb +0 -92
- data/lib/release.rhtml +0 -69
- data/lib/style.css +0 -127
- data/lib/trollop.rb +0 -518
- data/lib/unassigned.rhtml +0 -31
- data/lib/vendor/yaml_waml.rb +0 -28
@@ -1,92 +0,0 @@
|
|
1
|
-
module Ditz
|
2
|
-
class Issue
|
3
|
-
field :claimer, :ask => false
|
4
|
-
|
5
|
-
def claim who, comment, force=false
|
6
|
-
raise Error, "already claimed by #{claimer}" if claimer && !force
|
7
|
-
log "issue claimed", who, comment
|
8
|
-
self.claimer = who
|
9
|
-
end
|
10
|
-
|
11
|
-
def unclaim who, comment, force=false
|
12
|
-
raise Error, "not claimed" unless claimer
|
13
|
-
raise Error, "can only be unclaimed by #{claimer}" unless claimer == who || force
|
14
|
-
if claimer == who
|
15
|
-
log "issue unclaimed", who, comment
|
16
|
-
else
|
17
|
-
log "unassigned from #{claimer}", who, comment
|
18
|
-
end
|
19
|
-
self.claimer = nil
|
20
|
-
end
|
21
|
-
|
22
|
-
def claimed?; claimer end
|
23
|
-
def unclaimed?; !claimed? end
|
24
|
-
end
|
25
|
-
|
26
|
-
class ScreenView
|
27
|
-
add_to_view :issue_summary do |issue, config|
|
28
|
-
" Claimed by: #{issue.claimer || 'none'}\n"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
class HtmlView
|
33
|
-
add_to_view :issue_summary do |issue, config|
|
34
|
-
next unless issue.git_branch
|
35
|
-
[<<EOS, { :issue => issue, :url_prefix => config.git_branch_url_prefix }]
|
36
|
-
<tr>
|
37
|
-
<td class='attrname'>Claimed by:</td>
|
38
|
-
<td class='attrval'>h(issue.claimer) %></td>
|
39
|
-
</tr>
|
40
|
-
EOS
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
class Operator
|
45
|
-
operation :claim, "Claim an issue for yourself", :issue do
|
46
|
-
opt :force, "Claim this issue even if someone else has claimed it", :default => false
|
47
|
-
end
|
48
|
-
def claim project, config, opts, issue
|
49
|
-
puts "Claiming issue #{issue.name}: #{issue.title}."
|
50
|
-
comment = ask_multiline "Comments" unless $opts[:no_comment]
|
51
|
-
issue.claim config.user, comment, opts[:force]
|
52
|
-
puts "Issue #{issue.name} marked as claimed by #{config.user}"
|
53
|
-
end
|
54
|
-
|
55
|
-
operation :unclaim, "Unclaim a claimed issue", :issue do
|
56
|
-
opt :force, "Unclaim this issue even if it's claimed by someone else", :default => false
|
57
|
-
end
|
58
|
-
def unclaim project, config, opts, issue
|
59
|
-
puts "Unclaiming issue #{issue.name}: #{issue.title}."
|
60
|
-
comment = ask_multiline "Comments" unless $opts[:no_comment]
|
61
|
-
issue.unclaim config.user, comment, opts[:force]
|
62
|
-
puts "Issue #{issue.name} marked as unclaimed."
|
63
|
-
end
|
64
|
-
|
65
|
-
operation :claimed, "Show all issues claimed by you" do
|
66
|
-
opt :all, "Show all issues, not just open ones"
|
67
|
-
end
|
68
|
-
def claimed project, config, opts
|
69
|
-
puts "#{opts[:all] ? "All" : "Open"} issues claimed by you:"
|
70
|
-
issues = project.issues.select { |i| i.claimer == config.user && (opts[:all] || i.open?) }
|
71
|
-
if issues.empty?
|
72
|
-
puts "No issues."
|
73
|
-
else
|
74
|
-
print_todo_list_by_release_for project, issues
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
operation :unclaimed, "Show all unclaimed issues" do
|
79
|
-
opt :all, "Show all issues, not just open ones"
|
80
|
-
end
|
81
|
-
def unclaimed project, config, opts
|
82
|
-
puts "Unclaimed#{opts[:all] ? "" : ", open"} issues:"
|
83
|
-
issues = project.issues.select { |i| i.claimer.nil? && (opts[:all] || i.open?) }
|
84
|
-
if issues.empty?
|
85
|
-
puts "No issues."
|
86
|
-
else
|
87
|
-
print_todo_list_by_release_for project, issues
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
end
|
data/lib/release.rhtml
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
-
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
-
|
4
|
-
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
5
|
-
<head>
|
6
|
-
<title><%= project.name %> release <%= release.name %></title>
|
7
|
-
<meta http-equiv="Content-Type" content="text/html; charset=utf8" />
|
8
|
-
<link rel="stylesheet" href="style.css" type="text/css" />
|
9
|
-
</head>
|
10
|
-
<body>
|
11
|
-
|
12
|
-
<div><%= link_to "index", "« #{project.name} project page" %></div>
|
13
|
-
|
14
|
-
<h1><%= project.name %> release <%= release.name %></h1>
|
15
|
-
|
16
|
-
<table>
|
17
|
-
<tr>
|
18
|
-
<td class="attrname">Status:</td>
|
19
|
-
<td class="attrval"><%= release.status %></td>
|
20
|
-
</tr>
|
21
|
-
<% if release.released? %>
|
22
|
-
<tr>
|
23
|
-
<td class="attrname">Release time:</td>
|
24
|
-
<td class="attrval"><%= release.release_time %></td>
|
25
|
-
</tr>
|
26
|
-
<% end %>
|
27
|
-
<tr>
|
28
|
-
<%
|
29
|
-
num_done = issues.count_of { |i| i.closed? }
|
30
|
-
pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
|
31
|
-
%>
|
32
|
-
<td class="attrname">Completion:</td>
|
33
|
-
<td class="attrval"><%= sprintf "%.0f%%", pct_done %></td>
|
34
|
-
</tr>
|
35
|
-
</table>
|
36
|
-
|
37
|
-
<h2>Issues</h2>
|
38
|
-
<% if issues.empty? %>
|
39
|
-
<p>No issues assigned to this release.</p>
|
40
|
-
<% else %>
|
41
|
-
<%= render "issue_table", :show_component => false, :show_release => false %>
|
42
|
-
<% end %>
|
43
|
-
|
44
|
-
<h2>Release log</h2>
|
45
|
-
<table>
|
46
|
-
<% release.log_events.each_with_index do |(time, who, what, comment), i| %>
|
47
|
-
<% if i % 2 == 0 %>
|
48
|
-
<tr class="logentryeven">
|
49
|
-
<% else %>
|
50
|
-
<tr class="logentryodd">
|
51
|
-
<% end %>
|
52
|
-
<td class="logtime"><%=h time %></td>
|
53
|
-
<td class="logwho"><%=obscured_email who %></td>
|
54
|
-
<td class="logwhat"><%=h what %></td>
|
55
|
-
</tr>
|
56
|
-
<tr><td colspan="3" class="logcomment">
|
57
|
-
<% if comment.empty? %>
|
58
|
-
<% else %>
|
59
|
-
<%= link_issue_names project, p(comment) %>
|
60
|
-
<% end %>
|
61
|
-
</td></tr>
|
62
|
-
<% end %>
|
63
|
-
</table>
|
64
|
-
|
65
|
-
<p class="footer">Generated by <a
|
66
|
-
href="http://ditz.rubyforge.org/">ditz</a>.</p>
|
67
|
-
|
68
|
-
</body>
|
69
|
-
</html>
|
data/lib/style.css
DELETED
@@ -1,127 +0,0 @@
|
|
1
|
-
body {
|
2
|
-
background: white;
|
3
|
-
color: #202020;
|
4
|
-
margin-bottom: 2em;
|
5
|
-
margin-left: auto;
|
6
|
-
margin-right: auto;
|
7
|
-
width: 90%;
|
8
|
-
}
|
9
|
-
|
10
|
-
div.wrapper {
|
11
|
-
}
|
12
|
-
|
13
|
-
div.footer {
|
14
|
-
}
|
15
|
-
|
16
|
-
a, a:visited {
|
17
|
-
border-bottom: 1px dotted #03c;
|
18
|
-
background: inherit;
|
19
|
-
text-decoration: none;
|
20
|
-
color: #03c;
|
21
|
-
}
|
22
|
-
|
23
|
-
ul {
|
24
|
-
list-style-type: none;
|
25
|
-
padding: 0;
|
26
|
-
}
|
27
|
-
|
28
|
-
p {
|
29
|
-
width: 40em;
|
30
|
-
}
|
31
|
-
|
32
|
-
.dimmed {
|
33
|
-
color: #bbb;
|
34
|
-
}
|
35
|
-
|
36
|
-
div.on-the-left {
|
37
|
-
float: left;
|
38
|
-
width: 39%;
|
39
|
-
}
|
40
|
-
|
41
|
-
div.on-the-right {
|
42
|
-
float: right;
|
43
|
-
width: 59%;
|
44
|
-
}
|
45
|
-
|
46
|
-
table {
|
47
|
-
border-style: none;
|
48
|
-
border-spacing: 0;
|
49
|
-
}
|
50
|
-
|
51
|
-
td {
|
52
|
-
border-width: 0;
|
53
|
-
border-style: none;
|
54
|
-
padding-right: 0.5em;
|
55
|
-
padding-left: 0.5em;
|
56
|
-
}
|
57
|
-
|
58
|
-
tr {
|
59
|
-
padding-bottom: 5em;
|
60
|
-
}
|
61
|
-
|
62
|
-
h1 {
|
63
|
-
padding: 0.2em;
|
64
|
-
margin-left: -1em;
|
65
|
-
margin-right: 1em;
|
66
|
-
background: #abc;
|
67
|
-
}
|
68
|
-
|
69
|
-
.attrname {
|
70
|
-
text-align: right;
|
71
|
-
}
|
72
|
-
|
73
|
-
.issuestatus_closed {
|
74
|
-
background-color: #afa;
|
75
|
-
text-align: center;
|
76
|
-
}
|
77
|
-
|
78
|
-
.issuestatus_in_progress {
|
79
|
-
background-color: #ffa;
|
80
|
-
text-align: center;
|
81
|
-
}
|
82
|
-
|
83
|
-
.issuestatus_paused {
|
84
|
-
background-color: #ffa;
|
85
|
-
text-align: center;
|
86
|
-
}
|
87
|
-
|
88
|
-
.issuestatus_unstarted {
|
89
|
-
background-color: #faa;
|
90
|
-
text-align: center;
|
91
|
-
}
|
92
|
-
|
93
|
-
.issuestatus_closed a {
|
94
|
-
color: #202020;
|
95
|
-
}
|
96
|
-
|
97
|
-
.issuestatus_in_progress a {
|
98
|
-
color: #202020;
|
99
|
-
}
|
100
|
-
|
101
|
-
.issuestatus_paused a {
|
102
|
-
color: #202020;
|
103
|
-
}
|
104
|
-
|
105
|
-
.issuestatus_unstarted a {
|
106
|
-
color: #202020;
|
107
|
-
}
|
108
|
-
|
109
|
-
.logwhat {
|
110
|
-
}
|
111
|
-
|
112
|
-
.logentryeven {
|
113
|
-
background: #eee;
|
114
|
-
}
|
115
|
-
|
116
|
-
.logentryodd {
|
117
|
-
background: #eee;
|
118
|
-
}
|
119
|
-
|
120
|
-
.logcomment {
|
121
|
-
padding-left: 3em;
|
122
|
-
}
|
123
|
-
|
124
|
-
p.footer {
|
125
|
-
font-size: small;
|
126
|
-
}
|
127
|
-
|
data/lib/trollop.rb
DELETED
@@ -1,518 +0,0 @@
|
|
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:: GNU GPL version 2
|
5
|
-
|
6
|
-
module Trollop
|
7
|
-
|
8
|
-
VERSION = "1.8.2"
|
9
|
-
|
10
|
-
## Thrown by Parser in the event of a commandline error. Not needed if
|
11
|
-
## you're using the Trollop::options entry.
|
12
|
-
class CommandlineError < StandardError; end
|
13
|
-
|
14
|
-
## Thrown by Parser if the user passes in '-h' or '--help'. Handled
|
15
|
-
## automatically by Trollop#options.
|
16
|
-
class HelpNeeded < StandardError; end
|
17
|
-
|
18
|
-
## Thrown by Parser if the user passes in '-h' or '--version'. Handled
|
19
|
-
## automatically by Trollop#options.
|
20
|
-
class VersionNeeded < StandardError; end
|
21
|
-
|
22
|
-
## Regex for floating point numbers
|
23
|
-
FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
|
24
|
-
|
25
|
-
## Regex for parameters
|
26
|
-
PARAM_RE = /^-(-|\.$|[^\d\.])/
|
27
|
-
|
28
|
-
## The commandline parser. In typical usage, the methods in this class
|
29
|
-
## will be handled internally by Trollop#options, in which case only the
|
30
|
-
## methods #opt, #banner and #version, #depends, and #conflicts will
|
31
|
-
## typically be called.
|
32
|
-
class Parser
|
33
|
-
## The set of values specifiable as the :type parameter to #opt.
|
34
|
-
TYPES = [:flag, :boolean, :bool, :int, :integer, :string, :double, :float]
|
35
|
-
|
36
|
-
## :nodoc:
|
37
|
-
INVALID_SHORT_ARG_REGEX = /[\d-]/
|
38
|
-
|
39
|
-
## The values from the commandline that were not interpreted by #parse.
|
40
|
-
attr_reader :leftovers
|
41
|
-
|
42
|
-
## The complete configuration hashes for each option. (Mainly useful
|
43
|
-
## for testing.)
|
44
|
-
attr_reader :specs
|
45
|
-
|
46
|
-
## Initializes the parser, and instance-evaluates any block given.
|
47
|
-
def initialize *a, &b
|
48
|
-
@version = nil
|
49
|
-
@leftovers = []
|
50
|
-
@specs = {}
|
51
|
-
@long = {}
|
52
|
-
@short = {}
|
53
|
-
@order = []
|
54
|
-
@constraints = []
|
55
|
-
@stop_words = []
|
56
|
-
@stop_on_unknown = false
|
57
|
-
|
58
|
-
#instance_eval(&b) if b # can't take arguments
|
59
|
-
cloaker(&b).bind(self).call(*a) if b
|
60
|
-
end
|
61
|
-
|
62
|
-
## Add an option. 'name' is the argument name, a unique identifier
|
63
|
-
## for the option that you will use internally. 'desc' a string
|
64
|
-
## description which will be displayed in help messages. Takes the
|
65
|
-
## following optional arguments:
|
66
|
-
##
|
67
|
-
## * :long: Specify the long form of the argument, i.e. the form
|
68
|
-
## with two dashes. If unspecified, will be automatically derived
|
69
|
-
## based on the argument name.
|
70
|
-
## * :short: Specify the short form of the argument, i.e. the form
|
71
|
-
## with one dash. If unspecified, will be automatically derived
|
72
|
-
## based on the argument name.
|
73
|
-
## * :type: Require that the argument take a parameter of type
|
74
|
-
## 'type'. Can by any member of the TYPES constant or a
|
75
|
-
## corresponding class (e.g. Integer for :int). If unset, the
|
76
|
-
## default argument type is :flag, meaning the argument does not
|
77
|
-
## take a parameter. Not necessary if :default: is specified.
|
78
|
-
## * :default: Set the default value for an argument. Without a
|
79
|
-
## default value, the hash returned by #parse (and thus
|
80
|
-
## Trollop#options) will not contain the argument unless it is
|
81
|
-
## given on the commandline. The argument type is derived
|
82
|
-
## automatically from the class of the default value given, if
|
83
|
-
## any. Specifying a :flag argument on the commandline whose
|
84
|
-
## default value is true will change its value to false.
|
85
|
-
## * :required: if set to true, the argument must be provided on the
|
86
|
-
## commandline.
|
87
|
-
def opt name, desc="", opts={}
|
88
|
-
raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
|
89
|
-
|
90
|
-
## fill in :type
|
91
|
-
opts[:type] =
|
92
|
-
case opts[:type]
|
93
|
-
when :flag, :boolean, :bool; :flag
|
94
|
-
when :int, :integer; :int
|
95
|
-
when :string; :string
|
96
|
-
when :double, :float; :float
|
97
|
-
when Class
|
98
|
-
case opts[:type].to_s # sigh... there must be a better way to do this
|
99
|
-
when 'TrueClass', 'FalseClass'; :flag
|
100
|
-
when 'String'; :string
|
101
|
-
when 'Integer'; :int
|
102
|
-
when 'Float'; :float
|
103
|
-
else
|
104
|
-
raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
|
105
|
-
end
|
106
|
-
when nil; nil
|
107
|
-
else
|
108
|
-
raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
|
109
|
-
end
|
110
|
-
|
111
|
-
type_from_default =
|
112
|
-
case opts[:default]
|
113
|
-
when Integer; :int
|
114
|
-
when Numeric; :float
|
115
|
-
when TrueClass, FalseClass; :flag
|
116
|
-
when String; :string
|
117
|
-
when nil; nil
|
118
|
-
else
|
119
|
-
raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
|
120
|
-
end
|
121
|
-
|
122
|
-
raise ArgumentError, ":type specification and default type don't match" if opts[:type] && type_from_default && opts[:type] != type_from_default
|
123
|
-
|
124
|
-
opts[:type] = (opts[:type] || type_from_default || :flag)
|
125
|
-
|
126
|
-
## fill in :long
|
127
|
-
opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
|
128
|
-
opts[:long] =
|
129
|
-
case opts[:long]
|
130
|
-
when /^--([^-].*)$/
|
131
|
-
$1
|
132
|
-
when /^[^-]/
|
133
|
-
opts[:long]
|
134
|
-
else
|
135
|
-
raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
|
136
|
-
end
|
137
|
-
raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
|
138
|
-
|
139
|
-
## fill in :short
|
140
|
-
opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
|
141
|
-
opts[:short] =
|
142
|
-
case opts[:short]
|
143
|
-
when nil
|
144
|
-
c = opts[:long].split(//).find { |c| c !~ INVALID_SHORT_ARG_REGEX && !@short.member?(c) }
|
145
|
-
raise ArgumentError, "can't generate a short option name for #{opts[:long].inspect}: out of unique characters" unless c
|
146
|
-
c
|
147
|
-
when /^-(.)$/
|
148
|
-
$1
|
149
|
-
when /^.$/
|
150
|
-
opts[:short]
|
151
|
-
when :none
|
152
|
-
nil
|
153
|
-
else
|
154
|
-
raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
|
155
|
-
end
|
156
|
-
if opts[:short]
|
157
|
-
raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
|
158
|
-
raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
|
159
|
-
end
|
160
|
-
|
161
|
-
## fill in :default for flags
|
162
|
-
opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
|
163
|
-
|
164
|
-
opts[:desc] ||= desc
|
165
|
-
@long[opts[:long]] = name
|
166
|
-
@short[opts[:short]] = name if opts[:short]
|
167
|
-
@specs[name] = opts
|
168
|
-
@order << [:opt, name]
|
169
|
-
end
|
170
|
-
|
171
|
-
## Sets the version string. If set, the user can request the version
|
172
|
-
## on the commandline. Should be of the form "<program name>
|
173
|
-
## <version number>".
|
174
|
-
def version s=nil; @version = s if s; @version end
|
175
|
-
|
176
|
-
## Adds text to the help display.
|
177
|
-
def banner s; @order << [:text, s] end
|
178
|
-
alias :text :banner
|
179
|
-
|
180
|
-
## Marks two (or more!) options as requiring each other. Only handles
|
181
|
-
## undirected (i.e., mutual) dependencies. Directed dependencies are
|
182
|
-
## better modeled with Trollop::die.
|
183
|
-
def depends *syms
|
184
|
-
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
185
|
-
@constraints << [:depends, syms]
|
186
|
-
end
|
187
|
-
|
188
|
-
## Marks two (or more!) options as conflicting.
|
189
|
-
def conflicts *syms
|
190
|
-
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
191
|
-
@constraints << [:conflicts, syms]
|
192
|
-
end
|
193
|
-
|
194
|
-
## Defines a set of words which cause parsing to terminate when encountered,
|
195
|
-
## such that any options to the left of the word are parsed as usual, and
|
196
|
-
## options to the right of the word are left intact.
|
197
|
-
##
|
198
|
-
## A typical use case would be for subcommand support, where these would be
|
199
|
-
## set to the list of subcommands. A subsequent Trollop invocation would
|
200
|
-
## then be used to parse subcommand options.
|
201
|
-
def stop_on *words
|
202
|
-
@stop_words = [*words].flatten
|
203
|
-
end
|
204
|
-
|
205
|
-
## Similar to stop_on, but stops on any unknown word when encountered (unless
|
206
|
-
## it is a parameter for an argument).
|
207
|
-
def stop_on_unknown
|
208
|
-
@stop_on_unknown = true
|
209
|
-
end
|
210
|
-
|
211
|
-
## yield successive arg, parameter pairs
|
212
|
-
def each_arg args # :nodoc:
|
213
|
-
remains = []
|
214
|
-
i = 0
|
215
|
-
|
216
|
-
until i >= args.length
|
217
|
-
if @stop_words.member? args[i]
|
218
|
-
remains += args[i .. -1]
|
219
|
-
return remains
|
220
|
-
end
|
221
|
-
case args[i]
|
222
|
-
when /^--$/ # arg terminator
|
223
|
-
remains += args[(i + 1) .. -1]
|
224
|
-
return remains
|
225
|
-
when /^--(\S+?)=(\S+)$/ # long argument with equals
|
226
|
-
yield "--#{$1}", $2
|
227
|
-
i += 1
|
228
|
-
when /^--(\S+)$/ # long argument
|
229
|
-
if args[i + 1] && args[i + 1] !~ PARAM_RE && !@stop_words.member?(args[i + 1])
|
230
|
-
take_an_argument = yield args[i], args[i + 1]
|
231
|
-
unless take_an_argument
|
232
|
-
if @stop_on_unknown
|
233
|
-
remains += args[i + 1 .. -1]
|
234
|
-
return remains
|
235
|
-
else
|
236
|
-
remains << args[i + 1]
|
237
|
-
end
|
238
|
-
end
|
239
|
-
i += 2
|
240
|
-
else # long argument no parameter
|
241
|
-
yield args[i], nil
|
242
|
-
i += 1
|
243
|
-
end
|
244
|
-
when /^-(\S+)$/ # one or more short arguments
|
245
|
-
shortargs = $1.split(//)
|
246
|
-
shortargs.each_with_index do |a, j|
|
247
|
-
if j == (shortargs.length - 1) && args[i + 1] && args[i + 1] !~ PARAM_RE && !@stop_words.member?(args[i + 1])
|
248
|
-
take_an_argument = yield "-#{a}", args[i + 1]
|
249
|
-
unless take_an_argument
|
250
|
-
if @stop_on_unknown
|
251
|
-
remains += args[i + 1 .. -1]
|
252
|
-
return remains
|
253
|
-
else
|
254
|
-
remains << args[i + 1]
|
255
|
-
end
|
256
|
-
end
|
257
|
-
i += 1 # once more below
|
258
|
-
else
|
259
|
-
yield "-#{a}", nil
|
260
|
-
end
|
261
|
-
end
|
262
|
-
i += 1
|
263
|
-
else
|
264
|
-
if @stop_on_unknown
|
265
|
-
remains += args[i .. -1]
|
266
|
-
return remains
|
267
|
-
else
|
268
|
-
remains << args[i]
|
269
|
-
i += 1
|
270
|
-
end
|
271
|
-
end
|
272
|
-
end
|
273
|
-
remains
|
274
|
-
end
|
275
|
-
|
276
|
-
def parse cmdline #:nodoc:
|
277
|
-
vals = {}
|
278
|
-
required = {}
|
279
|
-
found = {}
|
280
|
-
|
281
|
-
opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
|
282
|
-
opt :help, "Show this message" unless @specs[:help] || @long["help"]
|
283
|
-
|
284
|
-
@specs.each do |sym, opts|
|
285
|
-
required[sym] = true if opts[:required]
|
286
|
-
vals[sym] = opts[:default]
|
287
|
-
end
|
288
|
-
|
289
|
-
## resolve symbols
|
290
|
-
args = []
|
291
|
-
@leftovers = each_arg cmdline do |arg, param|
|
292
|
-
sym =
|
293
|
-
case arg
|
294
|
-
when /^-([^-])$/
|
295
|
-
@short[$1]
|
296
|
-
when /^--([^-]\S*)$/
|
297
|
-
@long[$1]
|
298
|
-
else
|
299
|
-
raise CommandlineError, "invalid argument syntax: '#{arg}'"
|
300
|
-
end
|
301
|
-
raise CommandlineError, "unknown argument '#{arg}'" unless sym
|
302
|
-
raise CommandlineError, "option '#{arg}' specified multiple times" if found[sym]
|
303
|
-
args << [sym, arg, param]
|
304
|
-
found[sym] = true
|
305
|
-
|
306
|
-
@specs[sym][:type] != :flag # take params on all except flags
|
307
|
-
end
|
308
|
-
|
309
|
-
## check for version and help args
|
310
|
-
raise VersionNeeded if args.any? { |sym, *a| sym == :version }
|
311
|
-
raise HelpNeeded if args.any? { |sym, *a| sym == :help }
|
312
|
-
|
313
|
-
## check constraint satisfaction
|
314
|
-
@constraints.each do |type, syms|
|
315
|
-
constraint_sym = syms.find { |sym| found[sym] }
|
316
|
-
next unless constraint_sym
|
317
|
-
|
318
|
-
case type
|
319
|
-
when :depends
|
320
|
-
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless found[sym] }
|
321
|
-
when :conflicts
|
322
|
-
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if found[sym] && sym != constraint_sym }
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
required.each do |sym, val|
|
327
|
-
raise CommandlineError, "option '#{sym}' must be specified" unless found[sym]
|
328
|
-
end
|
329
|
-
|
330
|
-
## parse parameters
|
331
|
-
args.each do |sym, arg, param|
|
332
|
-
opts = @specs[sym]
|
333
|
-
|
334
|
-
raise CommandlineError, "option '#{arg}' needs a parameter" unless param || opts[:type] == :flag
|
335
|
-
|
336
|
-
case opts[:type]
|
337
|
-
when :flag
|
338
|
-
vals[sym] = !opts[:default]
|
339
|
-
when :int
|
340
|
-
raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
|
341
|
-
vals[sym] = param.to_i
|
342
|
-
when :float
|
343
|
-
raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
|
344
|
-
vals[sym] = param.to_f
|
345
|
-
when :string
|
346
|
-
vals[sym] = param.to_s
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
vals
|
351
|
-
end
|
352
|
-
|
353
|
-
def width #:nodoc:
|
354
|
-
@width ||=
|
355
|
-
if $stdout.tty?
|
356
|
-
begin
|
357
|
-
require 'curses'
|
358
|
-
Curses::init_screen
|
359
|
-
x = Curses::cols
|
360
|
-
Curses::close_screen
|
361
|
-
x
|
362
|
-
rescue Exception
|
363
|
-
80
|
364
|
-
end
|
365
|
-
else
|
366
|
-
80
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
## Print the help message to 'stream'.
|
371
|
-
def educate stream=$stdout
|
372
|
-
width # just calculate it now; otherwise we have to be careful not to
|
373
|
-
# call this unless the cursor's at the beginning of a line.
|
374
|
-
|
375
|
-
left = {}
|
376
|
-
@specs.each do |name, spec|
|
377
|
-
left[name] = "--#{spec[:long]}" +
|
378
|
-
(spec[:short] ? ", -#{spec[:short]}" : "") +
|
379
|
-
case spec[:type]
|
380
|
-
when :flag; ""
|
381
|
-
when :int; " <i>"
|
382
|
-
when :string; " <s>"
|
383
|
-
when :float; " <f>"
|
384
|
-
end
|
385
|
-
end
|
386
|
-
|
387
|
-
leftcol_width = left.values.map { |s| s.length }.max || 0
|
388
|
-
rightcol_start = leftcol_width + 6 # spaces
|
389
|
-
|
390
|
-
unless @order.size > 0 && @order.first.first == :text
|
391
|
-
stream.puts "#@version\n" if @version
|
392
|
-
stream.puts "Options:"
|
393
|
-
end
|
394
|
-
|
395
|
-
@order.each do |what, opt|
|
396
|
-
if what == :text
|
397
|
-
stream.puts wrap(opt)
|
398
|
-
next
|
399
|
-
end
|
400
|
-
|
401
|
-
spec = @specs[opt]
|
402
|
-
stream.printf " %#{leftcol_width}s: ", left[opt]
|
403
|
-
desc = spec[:desc] +
|
404
|
-
if spec[:default]
|
405
|
-
if spec[:desc] =~ /\.$/
|
406
|
-
" (Default: #{spec[:default]})"
|
407
|
-
else
|
408
|
-
" (default: #{spec[:default]})"
|
409
|
-
end
|
410
|
-
else
|
411
|
-
""
|
412
|
-
end
|
413
|
-
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
|
414
|
-
end
|
415
|
-
end
|
416
|
-
|
417
|
-
def wrap_line str, opts={} # :nodoc:
|
418
|
-
prefix = opts[:prefix] || 0
|
419
|
-
width = opts[:width] || (self.width - 1)
|
420
|
-
start = 0
|
421
|
-
ret = []
|
422
|
-
until start > str.length
|
423
|
-
nextt =
|
424
|
-
if start + width >= str.length
|
425
|
-
str.length
|
426
|
-
else
|
427
|
-
x = str.rindex(/\s/, start + width)
|
428
|
-
x = str.index(/\s/, start) if x && x < start
|
429
|
-
x || str.length
|
430
|
-
end
|
431
|
-
ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
|
432
|
-
start = nextt + 1
|
433
|
-
end
|
434
|
-
ret
|
435
|
-
end
|
436
|
-
|
437
|
-
def wrap str, opts={} # :nodoc:
|
438
|
-
if str == ""
|
439
|
-
[""]
|
440
|
-
else
|
441
|
-
str.split("\n").map { |s| wrap_line s, opts }.flatten
|
442
|
-
end
|
443
|
-
end
|
444
|
-
|
445
|
-
## instance_eval but with ability to handle block arguments
|
446
|
-
## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
|
447
|
-
def cloaker &b #:nodoc:
|
448
|
-
(class << self; self; end).class_eval do
|
449
|
-
define_method :cloaker_, &b
|
450
|
-
meth = instance_method :cloaker_
|
451
|
-
remove_method :cloaker_
|
452
|
-
meth
|
453
|
-
end
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
## The top-level entry method into Trollop. Creates a Parser object,
|
458
|
-
## passes the block to it, then parses +args+ with it, handling any
|
459
|
-
## errors or requests for help or version information appropriately
|
460
|
-
## (and then exiting). Modifies +args+ in place. Returns a hash of
|
461
|
-
## option values.
|
462
|
-
##
|
463
|
-
## The block passed in should contain one or more calls to #opt
|
464
|
-
## (Parser#opt), one or more calls to text (Parser#text), and
|
465
|
-
## probably a call to version (Parser#version).
|
466
|
-
##
|
467
|
-
## See the synopsis in README.txt for examples.
|
468
|
-
def options args = ARGV, *a, &b
|
469
|
-
@p = Parser.new(*a, &b)
|
470
|
-
begin
|
471
|
-
vals = @p.parse args
|
472
|
-
args.clear
|
473
|
-
@p.leftovers.each { |l| args << l }
|
474
|
-
vals
|
475
|
-
rescue CommandlineError => e
|
476
|
-
$stderr.puts "Error: #{e.message}."
|
477
|
-
$stderr.puts "Try --help for help."
|
478
|
-
exit(-1)
|
479
|
-
rescue HelpNeeded
|
480
|
-
@p.educate
|
481
|
-
exit
|
482
|
-
rescue VersionNeeded
|
483
|
-
puts @p.version
|
484
|
-
exit
|
485
|
-
end
|
486
|
-
end
|
487
|
-
|
488
|
-
## Informs the user that their usage of 'arg' was wrong, as detailed by
|
489
|
-
## 'msg', and dies. Example:
|
490
|
-
##
|
491
|
-
## options do
|
492
|
-
## opt :volume, :default => 0.0
|
493
|
-
## end
|
494
|
-
##
|
495
|
-
## die :volume, "too loud" if opts[:volume] > 10.0
|
496
|
-
## die :volume, "too soft" if opts[:volume] < 0.1
|
497
|
-
##
|
498
|
-
## In the one-argument case, simply print that message, a notice
|
499
|
-
## about -h, and die. Example:
|
500
|
-
##
|
501
|
-
## options do
|
502
|
-
## opt :whatever # ...
|
503
|
-
## end
|
504
|
-
##
|
505
|
-
## Trollop::die "need at least one filename" if ARGV.empty?
|
506
|
-
def die arg, msg=nil
|
507
|
-
if msg
|
508
|
-
$stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
|
509
|
-
else
|
510
|
-
$stderr.puts "Error: #{arg}."
|
511
|
-
end
|
512
|
-
$stderr.puts "Try --help for help."
|
513
|
-
exit(-1)
|
514
|
-
end
|
515
|
-
|
516
|
-
module_function :options, :die
|
517
|
-
|
518
|
-
end # module
|