rant 0.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.
- data/COPYING +504 -0
- data/README +203 -0
- data/Rantfile +104 -0
- data/TODO +19 -0
- data/bin/rant +12 -0
- data/bin/rant-import +12 -0
- data/devel-notes +50 -0
- data/doc/configure.rdoc +40 -0
- data/doc/csharp.rdoc +74 -0
- data/doc/rant-import.rdoc +32 -0
- data/doc/rant.rdoc +24 -0
- data/doc/rantfile.rdoc +227 -0
- data/doc/rubyproject.rdoc +210 -0
- data/lib/rant.rb +9 -0
- data/lib/rant/cs_compiler.rb +334 -0
- data/lib/rant/import.rb +291 -0
- data/lib/rant/import/rubydoc.rb +125 -0
- data/lib/rant/import/rubypackage.rb +417 -0
- data/lib/rant/import/rubytest.rb +97 -0
- data/lib/rant/plugin/README +50 -0
- data/lib/rant/plugin/configure.rb +345 -0
- data/lib/rant/plugin/csharp.rb +275 -0
- data/lib/rant/plugin_methods.rb +41 -0
- data/lib/rant/rantenv.rb +217 -0
- data/lib/rant/rantfile.rb +664 -0
- data/lib/rant/rantlib.rb +1118 -0
- data/lib/rant/rantsys.rb +258 -0
- data/lib/rant/rantvar.rb +82 -0
- data/rantmethods.rb +79 -0
- data/run_import +7 -0
- data/run_rant +7 -0
- data/setup.rb +1360 -0
- data/test/Rantfile +2 -0
- data/test/plugin/configure/Rantfile +47 -0
- data/test/plugin/configure/test_configure.rb +58 -0
- data/test/plugin/csharp/Hello.cs +10 -0
- data/test/plugin/csharp/Rantfile +30 -0
- data/test/plugin/csharp/src/A.cs +8 -0
- data/test/plugin/csharp/src/B.cs +8 -0
- data/test/plugin/csharp/test_csharp.rb +99 -0
- data/test/project1/Rantfile +127 -0
- data/test/project1/test_project.rb +203 -0
- data/test/project2/buildfile +14 -0
- data/test/project2/rantfile.rb +20 -0
- data/test/project2/sub1/Rantfile +12 -0
- data/test/project2/test_project.rb +87 -0
- data/test/project_rb1/README +14 -0
- data/test/project_rb1/bin/wgrep +5 -0
- data/test/project_rb1/lib/wgrep.rb +56 -0
- data/test/project_rb1/rantfile.rb +30 -0
- data/test/project_rb1/test/tc_wgrep.rb +21 -0
- data/test/project_rb1/test/text +3 -0
- data/test/project_rb1/test_project_rb1.rb +153 -0
- data/test/test_env.rb +47 -0
- data/test/test_filetask.rb +57 -0
- data/test/test_lighttask.rb +49 -0
- data/test/test_metatask.rb +29 -0
- data/test/test_rant_interface.rb +65 -0
- data/test/test_sys.rb +61 -0
- data/test/test_task.rb +115 -0
- data/test/toplevel.rf +11 -0
- data/test/ts_all.rb +4 -0
- data/test/tutil.rb +95 -0
- metadata +133 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
|
2
|
+
# C# plugin for Rant.
|
3
|
+
|
4
|
+
require 'rant/plugin_methods'
|
5
|
+
require 'rant/cs_compiler'
|
6
|
+
|
7
|
+
module Rant
|
8
|
+
|
9
|
+
class Generators::Assembly < CsCompiler
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def rant_generate(app, clr, args, &block)
|
13
|
+
assembly = self.new(&block)
|
14
|
+
if args.size == 1
|
15
|
+
targ = args.first
|
16
|
+
# embed caller information for correct resolving
|
17
|
+
# of source Rantfile
|
18
|
+
if targ.is_a? Hash
|
19
|
+
targ[:__caller__] = clr
|
20
|
+
else
|
21
|
+
targ = { :__caller__ => clr, targ => [] }
|
22
|
+
end
|
23
|
+
app.prepare_task(targ, nil) { |name,pre,blk|
|
24
|
+
assembly.out = name
|
25
|
+
t = AssemblyTask.new(app, assembly, &block)
|
26
|
+
# TODO: optimize
|
27
|
+
pre.each { |e| t << e }
|
28
|
+
t
|
29
|
+
}
|
30
|
+
else
|
31
|
+
cinf = ::Rant::Lib.parse_caller_elem(clr)
|
32
|
+
app.abort(app.pos_text(cinf[:file], cinf[:ln]),
|
33
|
+
"Assembly takes one argument, " +
|
34
|
+
"which should be like one given to the `task' command.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def csc
|
39
|
+
@csc
|
40
|
+
end
|
41
|
+
def csc= new_csc
|
42
|
+
case new_csc
|
43
|
+
when CsCompiler
|
44
|
+
@csc = new_csc
|
45
|
+
when String
|
46
|
+
@csc = CsCompiler.new(
|
47
|
+
CsCompiler.cs_compiler_name(new_csc))
|
48
|
+
@csc.csc_bin = new_csc
|
49
|
+
when nil
|
50
|
+
@csc = nil
|
51
|
+
else
|
52
|
+
self.csc = new_csc.to_s
|
53
|
+
end
|
54
|
+
@csc
|
55
|
+
end
|
56
|
+
end
|
57
|
+
@csc = nil
|
58
|
+
|
59
|
+
# Maybe:
|
60
|
+
# ["object"]
|
61
|
+
# Compile to object code. Not *that* usual for .NET.
|
62
|
+
# ["dll"]
|
63
|
+
# Create a shared library (also called DLL).
|
64
|
+
# ["exe"]
|
65
|
+
# Create an executable.
|
66
|
+
attr_accessor :target
|
67
|
+
|
68
|
+
def initialize(comp = nil, &init_block)
|
69
|
+
super()
|
70
|
+
@target = nil
|
71
|
+
@init_block = init_block
|
72
|
+
take_common_attrs comp if comp
|
73
|
+
end
|
74
|
+
|
75
|
+
# Synonym for +out+.
|
76
|
+
def name
|
77
|
+
out
|
78
|
+
end
|
79
|
+
|
80
|
+
# Synonym for +out=+.
|
81
|
+
def name=(new_name)
|
82
|
+
out = new_name
|
83
|
+
end
|
84
|
+
|
85
|
+
# Take common attributes like +optimize+, +csc+ and similar
|
86
|
+
# from the compiler object +comp+.
|
87
|
+
def take_common_attrs comp
|
88
|
+
@csc_name = comp.csc_name
|
89
|
+
@long_name = comp.long_name
|
90
|
+
@csc = comp.csc
|
91
|
+
@debug = comp.debug
|
92
|
+
comp.defines.each { |e|
|
93
|
+
@defines << e unless @defines.include? e
|
94
|
+
}
|
95
|
+
comp.lib_link_pathes.each { |e|
|
96
|
+
@lib_link_pathes << e unless @lib_link_pathes.include? e
|
97
|
+
}
|
98
|
+
@optimize = comp.optimize
|
99
|
+
@warnings = comp.warnings
|
100
|
+
# TODO: we currently take unconditionally all misc- and
|
101
|
+
# compiler specific args
|
102
|
+
comp.misc_args.each { |e|
|
103
|
+
@misc_args << e unless @misc_args.include? e
|
104
|
+
}
|
105
|
+
comp.specific_args.each_pair { |k,v|
|
106
|
+
# k is a compiler name, v is a list of arguments
|
107
|
+
# specific to this compiler type.
|
108
|
+
cst = @specific_args[k]
|
109
|
+
unless cst
|
110
|
+
@specific_args[k] = v
|
111
|
+
next
|
112
|
+
end
|
113
|
+
v.each { |e|
|
114
|
+
cst << e unless cst.include? e
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
# Call the initialization block and intialize compiler
|
120
|
+
# interface.
|
121
|
+
def init
|
122
|
+
# setup compiler interface
|
123
|
+
comp = Plugin::Csharp.csc_for_assembly(self) || self.class.csc
|
124
|
+
take_common_attrs comp if comp
|
125
|
+
|
126
|
+
# call initialization block
|
127
|
+
@init_block[self] if @init_block
|
128
|
+
|
129
|
+
# set target type
|
130
|
+
unless @target
|
131
|
+
@target = case @out
|
132
|
+
when /\.exe$/i: "exe"
|
133
|
+
when /\.dll$/i: "dll"
|
134
|
+
when /\.obj$/i: "object"
|
135
|
+
else "exe"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
# TODO: verify some attributes like @target
|
139
|
+
end
|
140
|
+
|
141
|
+
def compile
|
142
|
+
::Rant::Sys.sh(self.send("cmd_" + @target))
|
143
|
+
end
|
144
|
+
|
145
|
+
end # class Generators::Assembly
|
146
|
+
|
147
|
+
class AssemblyTask < FileTask
|
148
|
+
def initialize(app, assembly)
|
149
|
+
@assembly = assembly
|
150
|
+
super(app, @assembly.out) { |t|
|
151
|
+
app.context.instance_eval {
|
152
|
+
sys.sh assembly.send("cmd_" + assembly.target)
|
153
|
+
}
|
154
|
+
}
|
155
|
+
end
|
156
|
+
def resolve_prerequisites
|
157
|
+
@assembly.init
|
158
|
+
@pre.concat(@assembly.sources)
|
159
|
+
@pre.concat(@assembly.resources) if @assembly.resources
|
160
|
+
super
|
161
|
+
end
|
162
|
+
### experimental ###
|
163
|
+
def invoke(force = false)
|
164
|
+
@assembly.init
|
165
|
+
@pre.concat(@assembly.sources)
|
166
|
+
@pre.concat(@assembly.resources) if @assembly.resources
|
167
|
+
super
|
168
|
+
end
|
169
|
+
####################
|
170
|
+
end
|
171
|
+
end # module Rant
|
172
|
+
|
173
|
+
module Rant::Plugin
|
174
|
+
|
175
|
+
# This plugin class is currently designed to be instantiated only
|
176
|
+
# once with +rant_plugin_new+.
|
177
|
+
class Csharp
|
178
|
+
include ::Rant::PluginMethods
|
179
|
+
|
180
|
+
@plugin_object = nil
|
181
|
+
class << self
|
182
|
+
|
183
|
+
def rant_plugin_new(app, cinf, *args, &block)
|
184
|
+
if args.size > 1
|
185
|
+
app.abort(app.pos_text(cinf[:file], cinf[:ln]),
|
186
|
+
"Csharp plugin takes only one argument.")
|
187
|
+
end
|
188
|
+
self.new(app, args.first, &block)
|
189
|
+
end
|
190
|
+
|
191
|
+
attr_accessor :plugin_object
|
192
|
+
|
193
|
+
def csc_for_assembly(task)
|
194
|
+
if @plugin_object
|
195
|
+
@plugin_object.csc_for_assembly(task)
|
196
|
+
else
|
197
|
+
nil
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Shortcut for rant_plugin_name.
|
203
|
+
attr_reader :name
|
204
|
+
# A "configure" plugin.
|
205
|
+
attr_accessor :config
|
206
|
+
# A compiler interface with settings resulting from config.
|
207
|
+
attr_reader :config_csc
|
208
|
+
|
209
|
+
def initialize(app, name = nil)
|
210
|
+
@name = name || rant_plugin_type
|
211
|
+
@app = app or raise ArgumentError, "no application given"
|
212
|
+
@config_csc = nil
|
213
|
+
@config = nil
|
214
|
+
|
215
|
+
self.class.plugin_object = self
|
216
|
+
|
217
|
+
yield self if block_given?
|
218
|
+
|
219
|
+
define_config_checks
|
220
|
+
end
|
221
|
+
|
222
|
+
def csc_for_assembly(assembly)
|
223
|
+
@config_csc ||= csc_from_config
|
224
|
+
@config_csc.nil? ? nil : @config_csc.dup
|
225
|
+
end
|
226
|
+
|
227
|
+
def define_config_checks
|
228
|
+
return unless @config
|
229
|
+
@config.check "csc" do |c|
|
230
|
+
c.default "cscc"
|
231
|
+
c.guess {
|
232
|
+
Rant::CsCompiler.look_for_cs_compiler
|
233
|
+
}
|
234
|
+
c.interact {
|
235
|
+
c.prompt "Command to invoke your C# Compiler: "
|
236
|
+
}
|
237
|
+
c.react {
|
238
|
+
c.msg "Using `#{c.value}' as C# compiler."
|
239
|
+
}
|
240
|
+
end
|
241
|
+
@config.check "csc-optimize" do |c|
|
242
|
+
c.default true
|
243
|
+
c.interact {
|
244
|
+
c.ask_yes_no "Optimize C# compilation?"
|
245
|
+
}
|
246
|
+
end
|
247
|
+
@config.check "csc-debug" do |c|
|
248
|
+
c.default false
|
249
|
+
c.interact {
|
250
|
+
c.ask_yes_no "Compile C# sources for debugging?"
|
251
|
+
}
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def csc_from_config
|
256
|
+
return nil unless @config
|
257
|
+
return nil unless @config.configured?
|
258
|
+
csc_bin = @config["csc"]
|
259
|
+
csc = Rant::CsCompiler.new
|
260
|
+
csc.csc = csc_bin
|
261
|
+
csc.optimize = @config["csc-optimize"]
|
262
|
+
csc.debug = @config["csc-debug"]
|
263
|
+
csc
|
264
|
+
end
|
265
|
+
|
266
|
+
###### methods override from PluginMethods ###################
|
267
|
+
def rant_plugin_type
|
268
|
+
"csharp"
|
269
|
+
end
|
270
|
+
def rant_plugin_name
|
271
|
+
@name
|
272
|
+
end
|
273
|
+
##############################################################
|
274
|
+
end # class Csharp
|
275
|
+
end # module Rant::Plugin
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
require 'rant/rantlib'
|
3
|
+
|
4
|
+
# This module defines all instance methods required for an Rant
|
5
|
+
# plugin. Additionally, each plugin class has to define the class
|
6
|
+
# method +plugin_create+.
|
7
|
+
#
|
8
|
+
# Include this module in your plugin class to ensure your plugin won't
|
9
|
+
# break when Rant requires new methods.
|
10
|
+
module Rant::PluginMethods
|
11
|
+
# The type of your plugin as string.
|
12
|
+
def rant_plugin_type
|
13
|
+
"rant plugin"
|
14
|
+
end
|
15
|
+
# Please override this method. This is used as a name for your
|
16
|
+
# plugin instance.
|
17
|
+
def rant_plugin_name
|
18
|
+
"rant plugin object"
|
19
|
+
end
|
20
|
+
# This is used for verification. Usually you don't want to change
|
21
|
+
# this for your plugin :-)
|
22
|
+
def rant_plugin?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
# Called immediately after registration.
|
26
|
+
def rant_plugin_init
|
27
|
+
end
|
28
|
+
# Called before rant runs the first task.
|
29
|
+
def rant_start
|
30
|
+
end
|
31
|
+
# Called when rant *successfully* processed all required tasks.
|
32
|
+
def rant_done
|
33
|
+
end
|
34
|
+
# You should "shut down" your plugin as response to this method.
|
35
|
+
def rant_plugin_stop
|
36
|
+
end
|
37
|
+
# Called immediately before the rant application return control to
|
38
|
+
# the caller.
|
39
|
+
def rant_quit
|
40
|
+
end
|
41
|
+
end # module Rant::PluginMethods
|
data/lib/rant/rantenv.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
module Rant end
|
6
|
+
|
7
|
+
class Rant::Path
|
8
|
+
attr_reader :path
|
9
|
+
def initialize path, abs_path = nil
|
10
|
+
@path = path or raise ArgumentError, "path not given"
|
11
|
+
@abs_path = abs_path
|
12
|
+
end
|
13
|
+
def to_s
|
14
|
+
@path.dup
|
15
|
+
end
|
16
|
+
def to_str
|
17
|
+
@path.dup
|
18
|
+
end
|
19
|
+
def exist?
|
20
|
+
File.exist? @path
|
21
|
+
end
|
22
|
+
def file?
|
23
|
+
test ?f, @path
|
24
|
+
end
|
25
|
+
def dir?
|
26
|
+
test ?d, @path
|
27
|
+
end
|
28
|
+
def mtime
|
29
|
+
File.mtime @path
|
30
|
+
end
|
31
|
+
def absolute_path
|
32
|
+
@abs_path ||= File.expand_path(@path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# This module provides some platform indenpendant
|
37
|
+
# (let's hope) environment information.
|
38
|
+
module Rant::Env
|
39
|
+
OS = ::Config::CONFIG['target']
|
40
|
+
RUBY = ::Config::CONFIG['ruby_install_name']
|
41
|
+
|
42
|
+
@@zip_bin = false
|
43
|
+
@@tar_bin = false
|
44
|
+
|
45
|
+
def on_windows?
|
46
|
+
OS =~ /mswin/i
|
47
|
+
end
|
48
|
+
|
49
|
+
def have_zip?
|
50
|
+
if @@zip_bin == false
|
51
|
+
@@zip_bin = find_bin "zip"
|
52
|
+
end
|
53
|
+
!@@zip_bin.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def have_tar?
|
57
|
+
if @@tar_bin == false
|
58
|
+
@@tar_bin = find_bin "tar"
|
59
|
+
end
|
60
|
+
!@@tar_bin.nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get an array with all pathes in the PATH
|
64
|
+
# environment variable.
|
65
|
+
def pathes
|
66
|
+
# Windows doesn't care about case in environment variables,
|
67
|
+
# but the ENV hash does!
|
68
|
+
path = on_windows? ? ENV["Path"] : ENV["PATH"]
|
69
|
+
return [] unless path
|
70
|
+
if on_windows?
|
71
|
+
path.split(";")
|
72
|
+
else
|
73
|
+
path.split(":")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Searches for bin_name on path and returns
|
78
|
+
# an absolute path if successfull or nil
|
79
|
+
# if an executable called bin_name couldn't be found.
|
80
|
+
def find_bin bin_name
|
81
|
+
if on_windows?
|
82
|
+
bin_name_exe = nil
|
83
|
+
if bin_name !~ /\.[^\.]{1,3}$/i
|
84
|
+
bin_name_exe = bin_name + ".exe"
|
85
|
+
end
|
86
|
+
pathes.each { |dir|
|
87
|
+
file = File.join(dir, bin_name)
|
88
|
+
return file if test(?f, file)
|
89
|
+
if bin_name_exe
|
90
|
+
file = File.join(dir, bin_name_exe)
|
91
|
+
return file if test(?f, file)
|
92
|
+
end
|
93
|
+
}
|
94
|
+
else
|
95
|
+
pathes.each { |dir|
|
96
|
+
file = File.join(dir, bin_name)
|
97
|
+
return file if test(?x, file)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add quotes to a path and replace File::Separators if necessary.
|
104
|
+
def shell_path path
|
105
|
+
# TODO: check for more characters when deciding wheter to use
|
106
|
+
# quotes.
|
107
|
+
if on_windows?
|
108
|
+
path = path.tr("/", "\\")
|
109
|
+
if path.include? ' '
|
110
|
+
'"' + path + '"'
|
111
|
+
else
|
112
|
+
path
|
113
|
+
end
|
114
|
+
else
|
115
|
+
if path.include? ' '
|
116
|
+
"'" + path + "'"
|
117
|
+
else
|
118
|
+
path
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
extend self
|
124
|
+
end # module Rant::Env
|
125
|
+
|
126
|
+
module Rant::Console
|
127
|
+
RANT_PREFIX = "rant: "
|
128
|
+
ERROR_PREFIX = "[ERROR] "
|
129
|
+
WARN_PREFIX = "[WARNING] "
|
130
|
+
def msg_prefix
|
131
|
+
if defined? @msg_prefix and @msg_prefix
|
132
|
+
@msg_prefix
|
133
|
+
else
|
134
|
+
RANT_PREFIX
|
135
|
+
end
|
136
|
+
end
|
137
|
+
def msg *text
|
138
|
+
pre = msg_prefix
|
139
|
+
text = text.join("\n" + ' ' * pre.length)
|
140
|
+
$stderr.puts(pre + text)
|
141
|
+
end
|
142
|
+
def err_msg *text
|
143
|
+
pre = msg_prefix + ERROR_PREFIX
|
144
|
+
text = text.join("\n" + ' ' * pre.length)
|
145
|
+
$stderr.puts(pre + text)
|
146
|
+
end
|
147
|
+
def warn_msg *text
|
148
|
+
pre = msg_prefix + WARN_PREFIX
|
149
|
+
text = text.join("\n" + ' ' * pre.length)
|
150
|
+
$stderr.puts(pre + text)
|
151
|
+
end
|
152
|
+
def ask_yes_no text
|
153
|
+
$stderr.print msg_prefix + text + " [y|n] "
|
154
|
+
case $stdin.readline
|
155
|
+
when /y|yes/i: true
|
156
|
+
when /n|no/i: false
|
157
|
+
else
|
158
|
+
$stderr.puts(' ' * msg_prefix.length +
|
159
|
+
"Please answer with `yes' or `no'")
|
160
|
+
ask_yes_no text
|
161
|
+
end
|
162
|
+
end
|
163
|
+
def prompt text
|
164
|
+
$stderr.print msg_prefix + text
|
165
|
+
input = $stdin.readline
|
166
|
+
input ? input.chomp : input
|
167
|
+
end
|
168
|
+
def option_listing opts
|
169
|
+
rs = ""
|
170
|
+
opts.each { |lopt, *opt_a|
|
171
|
+
if opt_a.size == 2
|
172
|
+
# no short option
|
173
|
+
mode, desc = opt_a
|
174
|
+
else
|
175
|
+
sopt, mode, desc = opt_a
|
176
|
+
end
|
177
|
+
next unless desc # "private" option
|
178
|
+
optstr = ""
|
179
|
+
arg = nil
|
180
|
+
if mode == GetoptLong::REQUIRED_ARGUMENT
|
181
|
+
if desc =~ /(\b[A-Z_]{2,}\b)/
|
182
|
+
arg = $1
|
183
|
+
end
|
184
|
+
end
|
185
|
+
if lopt
|
186
|
+
optstr << lopt
|
187
|
+
if arg
|
188
|
+
optstr << " " << arg
|
189
|
+
end
|
190
|
+
optstr = optstr.ljust(30)
|
191
|
+
end
|
192
|
+
if sopt
|
193
|
+
optstr << " " unless optstr.empty?
|
194
|
+
optstr << sopt
|
195
|
+
if arg
|
196
|
+
optstr << " " << arg
|
197
|
+
end
|
198
|
+
end
|
199
|
+
rs << " #{optstr}\n"
|
200
|
+
rs << " #{desc.split("\n").join("\n ")}\n"
|
201
|
+
}
|
202
|
+
rs
|
203
|
+
end
|
204
|
+
|
205
|
+
extend self
|
206
|
+
end
|
207
|
+
|
208
|
+
class Rant::CustomConsole
|
209
|
+
include Rant::Console
|
210
|
+
|
211
|
+
def initialize msg_prefix = RANT_PREFIX
|
212
|
+
@msg_prefix = msg_prefix || ""
|
213
|
+
end
|
214
|
+
def msg_prefix=(str)
|
215
|
+
@msg_prefix = str || ""
|
216
|
+
end
|
217
|
+
end
|