qb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +146 -0
- data/.qb-options.yml +4 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +4 -0
- data/Rakefile +6 -0
- data/ansible.cfg +5 -0
- data/bin/console +14 -0
- data/bin/qb +287 -0
- data/bin/setup +9 -0
- data/dev/bin/.gitkeep +0 -0
- data/dev/bin/ungem +19 -0
- data/dev/setup.yml +58 -0
- data/lib/qb.rb +5 -0
- data/lib/qb/version.rb +3 -0
- data/library/git_mkdir.py +70 -0
- data/library/qb_facts.py +38 -0
- data/qb.gemspec +27 -0
- data/requirements.yml +2 -0
- data/roles/qb.gem/.qb-options.yml +4 -0
- data/roles/qb.gem/defaults/main.yml +17 -0
- data/roles/qb.gem/files/.gitkeep +0 -0
- data/roles/qb.gem/files/.rspec +2 -0
- data/roles/qb.gem/files/Rakefile +6 -0
- data/roles/qb.gem/files/setup +7 -0
- data/roles/qb.gem/filter_plugins/ruby_constantize.py +22 -0
- data/roles/qb.gem/meta/main.yml +35 -0
- data/roles/qb.gem/tasks/main.yml +105 -0
- data/roles/qb.gem/templates/.gitkeep +0 -0
- data/roles/qb.gem/templates/.travis.yml.j2 +4 -0
- data/roles/qb.gem/templates/BSD2-LICENSE.txt.j2 +23 -0
- data/roles/qb.gem/templates/BSD3-LICENSE.txt.j2 +27 -0
- data/roles/qb.gem/templates/Gemfile.j2 +4 -0
- data/roles/qb.gem/templates/MIT-LICENSE.txt.j2 +21 -0
- data/roles/qb.gem/templates/console.j2 +14 -0
- data/roles/qb.gem/templates/gemspec.j2 +36 -0
- data/roles/qb.gem/templates/module.rb.j2 +5 -0
- data/roles/qb.gem/templates/spec.rb.j2 +11 -0
- data/roles/qb.gem/templates/spec_helper.rb.j2 +2 -0
- data/roles/qb.gem/templates/version.rb.j2 +3 -0
- data/roles/qb.git_repo/defaults/main.yml +2 -0
- data/roles/qb.git_repo/meta/main.yml +5 -0
- data/roles/qb.git_repo/tasks/main.yml +9 -0
- data/roles/qb.gitignore/defaults/main.yml +4 -0
- data/roles/qb.gitignore/meta/main.yml +12 -0
- data/roles/qb.gitignore/tasks/main.yml +31 -0
- data/roles/qb.project/.qb-options.yml +3 -0
- data/roles/qb.project/defaults/main.yml +12 -0
- data/roles/qb.project/meta/main.yml +49 -0
- data/roles/qb.project/qb/get_dir +13 -0
- data/roles/qb.project/tasks/main.yml +102 -0
- data/roles/qb.project/templates/.gitkeep +0 -0
- data/roles/qb.project/templates/README.md.j2 +2 -0
- data/roles/qb.project/templates/setup.yml.j2 +52 -0
- data/roles/qb.role/.qb-options.yml +3 -0
- data/roles/qb.role/defaults/main.yml +9 -0
- data/roles/qb.role/meta/main.yml +31 -0
- data/roles/qb.role/tasks/main.yml +117 -0
- data/roles/qb.role/templates/.gitkeep +0 -0
- data/roles/qb.role/templates/defaults/main.yml.j2 +2 -0
- data/roles/qb.role/templates/handlers/main.yml.j2 +2 -0
- data/roles/qb.role/templates/meta/main.yml.j2 +5 -0
- data/roles/qb.role/templates/tasks/main.yml.j2 +2 -0
- data/roles/qb.role/templates/vars/main.yml.j2 +2 -0
- data/scratch/case.rb +38 -0
- data/temp.yml +19 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ed9e572283f9de3975f9d285ebf382ca92b81266
|
4
|
+
data.tar.gz: bc7d66d6988545c060af3b213edf6c2b1c3c0f8f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 02102a5039d84ade5dd8984da6fbbeaf9ec4bd87fae8976b7e5a2095c21bd520531ba76935991aec4940e7f905420dffb874b69288e94bb79a9e8979ce5da0fe
|
7
|
+
data.tar.gz: ae6e942153d39b7f2ff8efdd17902ef6bc2a6d2abdf747a617d53fecae2d6cdea51bc3e31ff8b712fdbacac0e75cae02e6b29432a2411e33946e8961d7203f0f
|
data/.gitignore
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# temp playbook created by qb for ansible-playbook to consume
|
2
|
+
/.qb-playbook.yml
|
3
|
+
/dev/repos
|
4
|
+
/dev/ref/repos
|
5
|
+
/tmp
|
6
|
+
# BEGIN Global/OSX.gitignore
|
7
|
+
.DS_Store
|
8
|
+
.AppleDouble
|
9
|
+
.LSOverride
|
10
|
+
|
11
|
+
# Icon must end with two
|
12
|
+
Icon
|
13
|
+
|
14
|
+
|
15
|
+
# Thumbnails
|
16
|
+
._*
|
17
|
+
|
18
|
+
# Files that might appear in the root of a volume
|
19
|
+
.DocumentRevisions-V100
|
20
|
+
.fseventsd
|
21
|
+
.Spotlight-V100
|
22
|
+
.TemporaryItems
|
23
|
+
.Trashes
|
24
|
+
.VolumeIcon.icns
|
25
|
+
|
26
|
+
# Directories potentially created on remote AFP share
|
27
|
+
.AppleDB
|
28
|
+
.AppleDesktop
|
29
|
+
Network Trash Folder
|
30
|
+
Temporary Items
|
31
|
+
.apdisk
|
32
|
+
# END Global/OSX.gitignore
|
33
|
+
# BEGIN Ruby.gitignore
|
34
|
+
/*.gem
|
35
|
+
*.rbc
|
36
|
+
/.config
|
37
|
+
/coverage/
|
38
|
+
/InstalledFiles
|
39
|
+
/pkg/
|
40
|
+
/spec/reports/
|
41
|
+
/spec/examples.txt
|
42
|
+
/test/tmp/
|
43
|
+
/test/version_tmp/
|
44
|
+
/tmp/
|
45
|
+
|
46
|
+
## Specific to RubyMotion:
|
47
|
+
.dat*
|
48
|
+
.repl_history
|
49
|
+
build/
|
50
|
+
|
51
|
+
## Documentation cache and generated files:
|
52
|
+
/.yardoc/
|
53
|
+
/_yardoc/
|
54
|
+
/doc/
|
55
|
+
/rdoc/
|
56
|
+
|
57
|
+
## Environment normalization:
|
58
|
+
/.bundle/
|
59
|
+
/vendor/bundle
|
60
|
+
/lib/bundler/man/
|
61
|
+
|
62
|
+
# for a library or gem, you might want to ignore these files since the code is
|
63
|
+
# intended to run in multiple environments; otherwise, check them in:
|
64
|
+
# Gemfile.lock
|
65
|
+
# .ruby-version
|
66
|
+
# .ruby-gemset
|
67
|
+
|
68
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
69
|
+
.rvmrc
|
70
|
+
# END Ruby.gitignore
|
71
|
+
# BEGIN Gem.gitignore
|
72
|
+
/Gemfile.lock
|
73
|
+
/.ruby-version
|
74
|
+
/.ruby-gemset
|
75
|
+
# END Gem.gitignore
|
76
|
+
# BEGIN Python.gitignore
|
77
|
+
# Byte-compiled / optimized / DLL files
|
78
|
+
__pycache__/
|
79
|
+
*.py[cod]
|
80
|
+
*$py.class
|
81
|
+
|
82
|
+
# C extensions
|
83
|
+
*.so
|
84
|
+
|
85
|
+
# Distribution / packaging
|
86
|
+
.Python
|
87
|
+
env/
|
88
|
+
build/
|
89
|
+
develop-eggs/
|
90
|
+
dist/
|
91
|
+
downloads/
|
92
|
+
eggs/
|
93
|
+
.eggs/
|
94
|
+
|
95
|
+
# ruby uses this!
|
96
|
+
# lib/
|
97
|
+
|
98
|
+
lib64/
|
99
|
+
parts/
|
100
|
+
sdist/
|
101
|
+
var/
|
102
|
+
*.egg-info/
|
103
|
+
.installed.cfg
|
104
|
+
*.egg
|
105
|
+
|
106
|
+
# PyInstaller
|
107
|
+
# Usually these files are written by a python script from a template
|
108
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
109
|
+
*.manifest
|
110
|
+
*.spec
|
111
|
+
|
112
|
+
# Installer logs
|
113
|
+
pip-log.txt
|
114
|
+
pip-delete-this-directory.txt
|
115
|
+
|
116
|
+
# Unit test / coverage reports
|
117
|
+
htmlcov/
|
118
|
+
.tox/
|
119
|
+
.coverage
|
120
|
+
.coverage.*
|
121
|
+
.cache
|
122
|
+
nosetests.xml
|
123
|
+
coverage.xml
|
124
|
+
*,cover
|
125
|
+
.hypothesis/
|
126
|
+
|
127
|
+
# Translations
|
128
|
+
*.mo
|
129
|
+
*.pot
|
130
|
+
|
131
|
+
# Django stuff:
|
132
|
+
*.log
|
133
|
+
local_settings.py
|
134
|
+
|
135
|
+
# Sphinx documentation
|
136
|
+
docs/_build/
|
137
|
+
|
138
|
+
# PyBuilder
|
139
|
+
target/
|
140
|
+
|
141
|
+
#Ipython Notebook
|
142
|
+
.ipynb_checkpoints
|
143
|
+
|
144
|
+
# pyenv
|
145
|
+
.python-version
|
146
|
+
# END Python.gitignore
|
data/.qb-options.yml
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 nrser
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
data/ansible.cfg
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "qb"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/qb
ADDED
@@ -0,0 +1,287 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'pp'
|
5
|
+
require 'yaml'
|
6
|
+
require 'optparse'
|
7
|
+
require 'json'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
require 'cmds'
|
11
|
+
|
12
|
+
# constants
|
13
|
+
# =========
|
14
|
+
|
15
|
+
ROOT = (Pathname.new(__FILE__).dirname + '..').expand_path
|
16
|
+
ROLES_DIR = ROOT + 'roles'
|
17
|
+
ROLES = Pathname.glob(ROLES_DIR + 'qb.*').map {|path| path.basename.to_s}
|
18
|
+
DEBUG_ARGS = ['-d', '--debug']
|
19
|
+
|
20
|
+
# globals
|
21
|
+
# =======
|
22
|
+
|
23
|
+
$debug = false
|
24
|
+
|
25
|
+
# @api util
|
26
|
+
# *pure*
|
27
|
+
#
|
28
|
+
# format a debug message with optional key / values to print
|
29
|
+
#
|
30
|
+
# @param msg [String] message to print.
|
31
|
+
# @param dump [Hash] optional hash of keys and vaues to dump.
|
32
|
+
def format msg, dump = {}
|
33
|
+
unless dump.empty?
|
34
|
+
msg += "\n" + dump.map {|k, v| " #{ k }: #{ v.inspect }" }.join("\n")
|
35
|
+
end
|
36
|
+
msg
|
37
|
+
end
|
38
|
+
|
39
|
+
def debug *args
|
40
|
+
return unless $debug
|
41
|
+
|
42
|
+
msg, values = case args.length
|
43
|
+
when 0
|
44
|
+
raise ArgumentError, "debug needs at least one argument"
|
45
|
+
when 1
|
46
|
+
if args[0].is_a? Hash
|
47
|
+
['', args[0]]
|
48
|
+
else
|
49
|
+
[args[0], {}]
|
50
|
+
end
|
51
|
+
when 2
|
52
|
+
[args[0], args[1]]
|
53
|
+
else
|
54
|
+
raise ArgumentError, "debug needs at least one argument"
|
55
|
+
end
|
56
|
+
|
57
|
+
$stderr.puts("DEBUG " + format(msg, values))
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_debug! args
|
61
|
+
if DEBUG_ARGS.any? {|arg| args.include? arg}
|
62
|
+
$debug = true
|
63
|
+
debug "ON"
|
64
|
+
DEBUG_ARGS.each {|arg| args.delete arg}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse! role_arg, var_prefix, vars, defaults, args
|
69
|
+
positional = vars.select do |var|
|
70
|
+
var['positional'] == true
|
71
|
+
end
|
72
|
+
|
73
|
+
positional_banner = if positional.empty?
|
74
|
+
''
|
75
|
+
else
|
76
|
+
' ' + positional.map {|var|
|
77
|
+
var['name'].upcase
|
78
|
+
}.join(' ')
|
79
|
+
end
|
80
|
+
|
81
|
+
options = {}
|
82
|
+
|
83
|
+
opt_parser = OptionParser.new do |opts|
|
84
|
+
opts.banner = "qb #{ role_arg } [OPTIONS]#{ positional_banner }"
|
85
|
+
|
86
|
+
vars.each do |var|
|
87
|
+
arg_name = var.fetch 'name'
|
88
|
+
var_name = "#{ var_prefix }_#{ arg_name }"
|
89
|
+
required = var['required'] || false
|
90
|
+
arg_style = required ? :REQUIRED : :OPTIONAL
|
91
|
+
|
92
|
+
# on_args = [arg_style]
|
93
|
+
on_args = []
|
94
|
+
|
95
|
+
if var['type'] == 'boolean'
|
96
|
+
if var['short']
|
97
|
+
on_args << "-#{ var['short'] }"
|
98
|
+
end
|
99
|
+
|
100
|
+
on_args << "--[no-]#{ var['name'] }"
|
101
|
+
|
102
|
+
else
|
103
|
+
ruby_type = case var['type']
|
104
|
+
when 'string'
|
105
|
+
String
|
106
|
+
else
|
107
|
+
raise ArgumentError, "bad type: #{ var['type'].inspect }"
|
108
|
+
end
|
109
|
+
|
110
|
+
if var['short']
|
111
|
+
on_args << "-#{ var['short'] } #{ arg_name.upcase }"
|
112
|
+
end
|
113
|
+
|
114
|
+
on_args << "--#{ var['name'] }=#{ arg_name.upcase }"
|
115
|
+
|
116
|
+
on_args << ruby_type
|
117
|
+
end
|
118
|
+
|
119
|
+
# description
|
120
|
+
if var.key? 'description'
|
121
|
+
on_args << var['description']
|
122
|
+
else
|
123
|
+
on_args << "set the #{ var_name } variable"
|
124
|
+
end
|
125
|
+
|
126
|
+
if defaults.key? var_name
|
127
|
+
on_args << "(defaults to #{ defaults[var_name] })"
|
128
|
+
end
|
129
|
+
|
130
|
+
debug "adding option", name: arg_name, on_args: on_args
|
131
|
+
|
132
|
+
opts.on(*on_args) do |value|
|
133
|
+
options[var['name']] = value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# No argument, shows at tail. This will print an options summary.
|
138
|
+
# Try it and see!
|
139
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
140
|
+
puts opts
|
141
|
+
exit
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
opt_parser.parse! args
|
146
|
+
|
147
|
+
# args.each_with_index do |value, index|
|
148
|
+
# var_name = positional[index]['name']
|
149
|
+
# if options.key? var_name
|
150
|
+
# raise ArgumentError, "don't supply #{ var_name } as option and positionaly"
|
151
|
+
# else
|
152
|
+
# options[var_name] = value
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
|
156
|
+
options
|
157
|
+
end
|
158
|
+
|
159
|
+
def main args
|
160
|
+
set_debug! args
|
161
|
+
debug args: args
|
162
|
+
|
163
|
+
role_arg = args.shift
|
164
|
+
debug "role arg" => role_arg
|
165
|
+
|
166
|
+
matches = ROLES.select {|role|
|
167
|
+
role.include? role_arg
|
168
|
+
}
|
169
|
+
debug "role matches" => matches
|
170
|
+
|
171
|
+
role = case matches.length
|
172
|
+
when 0
|
173
|
+
raise ArgumentError, "no roles match arg #{ role_arg.inspect }"
|
174
|
+
when 1
|
175
|
+
matches[0]
|
176
|
+
else
|
177
|
+
raise ArgumentError, "multiple role matches: #{ matches.inpsect }"
|
178
|
+
end
|
179
|
+
debug role: role
|
180
|
+
|
181
|
+
role_dir = ROLES_DIR + role
|
182
|
+
|
183
|
+
defaults = YAML.load (role_dir + 'defaults' + 'main.yml').read
|
184
|
+
meta = YAML.load (role_dir + 'meta' + 'main.yml').read
|
185
|
+
|
186
|
+
qb_info = meta['qb_info'] || {}
|
187
|
+
vars = qb_info['vars'] || []
|
188
|
+
var_prefix = qb_info['var_prefix'] || role.split('.').last
|
189
|
+
|
190
|
+
options = parse! role_arg, var_prefix, vars, defaults, args
|
191
|
+
|
192
|
+
debug options: options
|
193
|
+
|
194
|
+
# get the target dir
|
195
|
+
dir = case args.length
|
196
|
+
when 0
|
197
|
+
# in this case, a dir has not been provided
|
198
|
+
#
|
199
|
+
# in some cases (like projects) the dir can be figured out from other
|
200
|
+
# variables. i created a hacky-ass way of dealing with this:
|
201
|
+
#
|
202
|
+
# when the sole positional arg is missing, we look for a `qb/get_dir`
|
203
|
+
# executable in the role. if it exists, call it with the JSON encoded
|
204
|
+
# options passed over STDIN. if the execuable succeeds, the result is
|
205
|
+
# taken as dir.
|
206
|
+
#
|
207
|
+
get_dir_path = role_dir + 'qb' + 'get_dir'
|
208
|
+
|
209
|
+
unless get_dir_path.exist?
|
210
|
+
raise "no dir argument provided and no qb/get_dir exe found"
|
211
|
+
end
|
212
|
+
|
213
|
+
Cmds.chomp! get_dir_path.to_s do
|
214
|
+
JSON.dump options
|
215
|
+
end
|
216
|
+
|
217
|
+
when 1
|
218
|
+
# there is a single positional arg, which is used as dir
|
219
|
+
args[0]
|
220
|
+
|
221
|
+
else
|
222
|
+
# there are multiple positional args, which is not allowed
|
223
|
+
raise "can't supply more than one argument"
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
debug input_dir: dir
|
228
|
+
|
229
|
+
# normalize to expanded path (has no trailing slash)
|
230
|
+
dir = File.expand_path dir
|
231
|
+
|
232
|
+
debug normalized_dir: dir
|
233
|
+
|
234
|
+
# create the dir if it doesn't exist (so don't have to cover this in
|
235
|
+
# every role)
|
236
|
+
FileUtils.mkdir_p dir unless File.exists? dir
|
237
|
+
|
238
|
+
saved_options_path = Pathname.new(dir) + '.qb-options.yml'
|
239
|
+
|
240
|
+
saved_options = if saved_options_path.exist?
|
241
|
+
YAML.load saved_options_path.read
|
242
|
+
else
|
243
|
+
{}
|
244
|
+
end
|
245
|
+
|
246
|
+
if saved_options.key? role
|
247
|
+
options = saved_options[role].merge options
|
248
|
+
end
|
249
|
+
|
250
|
+
playbook_role = {'role' => role}
|
251
|
+
options.each do |arg_name, arg_value|
|
252
|
+
playbook_role["#{ var_prefix }_#{ arg_name }"] = arg_value
|
253
|
+
end
|
254
|
+
|
255
|
+
playbook_role['dir'] = dir
|
256
|
+
|
257
|
+
playbook = [
|
258
|
+
{
|
259
|
+
'hosts' => 'localhost',
|
260
|
+
'pre_tasks' => [
|
261
|
+
{'qb_facts' => nil},
|
262
|
+
],
|
263
|
+
'roles' => [
|
264
|
+
'yaegashi.blockinfile',
|
265
|
+
playbook_role
|
266
|
+
],
|
267
|
+
}
|
268
|
+
]
|
269
|
+
|
270
|
+
debug playbook: playbook
|
271
|
+
|
272
|
+
File.open './.qb-playbook.yml', 'w' do |f|
|
273
|
+
f.write YAML.dump(playbook)
|
274
|
+
end
|
275
|
+
|
276
|
+
unless options.empty?
|
277
|
+
saved_options[role] = options
|
278
|
+
FileUtils.mkdir_p saved_options_path.dirname unless saved_options_path.dirname.exist?
|
279
|
+
saved_options_path.open('w') do |f|
|
280
|
+
f.write YAML.dump(saved_options)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
exec "ansible-playbook ./.qb-playbook.yml"
|
285
|
+
end
|
286
|
+
|
287
|
+
main(ARGV) if __FILE__ == $0
|