gem-search 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 rochefort
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # gem-search
2
+
3
+ gem-search is a command line utitlity like rubygems('gem search').
4
+ You can see a download total gem and sort columns.
5
+
6
+
7
+ ## Installation
8
+
9
+ install it yourself as:
10
+
11
+ $ gem install gem-search
12
+
13
+ ## Usage
14
+
15
+ Use the gem-search as follows:
16
+
17
+ ### Sorted by gem name
18
+ gem-search `keyword'
19
+
20
+ e.g.:
21
+
22
+ ```
23
+ $ gem-search webkit
24
+ NAME DL(ver) DL(all)
25
+ -------------------------------------------------- -------- ---------
26
+ capybara-webkit (0.12.1) 24863 185214
27
+ capybara-webkit-remote (0.0.4) 209 838
28
+ gtk-webkit-ruby (0.0.3) 289 289
29
+ guard-jasmine-headless-webkit (0.3.2) 11964 17872
30
+ intentmedia-capybara-webkit (0.7.2.4) 370 1362
31
+ jasmine-headless-webkit (0.8.4) 19444 46147
32
+ otherinbox-capybara-webkit (0.12.0.1) 594 740
33
+ rspec-formatter-webkit (2.2.0) 438 3549
34
+ webkit-rspec-formatter (2.0.2) 613 1573
35
+ ```
36
+
37
+ ### Sorted by DL(ver)
38
+ gem-search `keyword' -s v
39
+
40
+ e.g.:
41
+
42
+ ```
43
+ $ gem-search factory-girl -s v
44
+ NAME DL(ver) DL(all)
45
+ -------------------------------------------------- -------- ---------
46
+ capybara-webkit (0.12.1) 24863 185214
47
+ jasmine-headless-webkit (0.8.4) 19444 46147
48
+ guard-jasmine-headless-webkit (0.3.2) 11964 17872
49
+ webkit-rspec-formatter (2.0.2) 613 1573
50
+ otherinbox-capybara-webkit (0.12.0.1) 594 740
51
+ rspec-formatter-webkit (2.2.0) 438 3549
52
+ intentmedia-capybara-webkit (0.7.2.4) 370 1362
53
+ gtk-webkit-ruby (0.0.3) 289 289
54
+ capybara-webkit-remote (0.0.4) 209 838
55
+ ```
56
+
57
+ ### Sorted by DL(all)
58
+
59
+ gem-search `keyword' -s a
60
+
61
+ e.g.:
62
+
63
+ ```
64
+ $ gem-search webkit -s a
65
+ NAME DL(ver) DL(all)
66
+ -------------------------------------------------- -------- ---------
67
+ capybara-webkit (0.12.1) 24863 185214
68
+ jasmine-headless-webkit (0.8.4) 19444 46147
69
+ guard-jasmine-headless-webkit (0.3.2) 11964 17872
70
+ rspec-formatter-webkit (2.2.0) 438 3549
71
+ webkit-rspec-formatter (2.0.2) 613 1573
72
+ intentmedia-capybara-webkit (0.7.2.4) 370 1362
73
+ capybara-webkit-remote (0.0.4) 209 838
74
+ otherinbox-capybara-webkit (0.12.0.1) 594 740
75
+ gtk-webkit-ruby (0.0.3) 289 289
76
+ ```
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/gem-search ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ require 'slop'
3
+
4
+ def opt_description(msgs)
5
+ ind = ' ' * 22
6
+ msgs.inject { |rtn, msg| rtn << "\n#{ind}#{msg}" }
7
+ end
8
+
9
+ opts = Slop.parse(help: true) do
10
+ banner "Usage: gem-search gem_name [options]\n"
11
+ on :s, :sort, opt_description([
12
+ 'Sort by name of item.',
13
+ ' [n]ame (default)',
14
+ ' [v]er (displayed name: DL(ver))',
15
+ ' [a]ll (displayed name: DL(all))'
16
+ ]), :argument => :optional
17
+ on :v, :version, 'Display the version.'
18
+ end
19
+
20
+ OPT_SORT = {
21
+ 'v' => 'version_downloads',
22
+ 'a' => 'downloads',
23
+ }
24
+
25
+ exit if opts['h']
26
+
27
+ $:.unshift File.dirname(__FILE__) + '/../lib'
28
+ require 'gem-search'
29
+ if opts['v']
30
+ puts "gem-search #{Gem::Search::VERSION}"
31
+ exit
32
+ end
33
+
34
+ gs = Gem::Search::CLI.new
35
+ gs.search(ARGV[0], OPT_SORT[opts['sort']])
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/gem-search/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["rochefort"]
6
+ gem.email = ["terasawan@gmail.com"]
7
+ gem.homepage = "https://github.com/rochefort/gem-search"
8
+ gem.summary = "search gems with using rubygems.org API"
9
+ gem.description = gem.summary
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "gem-search"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Gem::Search::VERSION
17
+
18
+ gem.add_dependency 'slop'
19
+ gem.add_dependency 'json'
20
+
21
+ gem.add_development_dependency 'webmock'
22
+ end
data/lib/gem-search.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'gem-search/version'
2
+ require 'gem-search/cli'
@@ -0,0 +1,67 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+
4
+ module Gem::Search
5
+ class CLI
6
+ BASE_URL = 'https://rubygems.org'
7
+ SEARCH_URL = "#{BASE_URL}/api/v1/search.json?query="
8
+ DEFAULT_RULED_LINE_SIZE = [50, 8, 9]
9
+
10
+ def search(query, opt_sort='name')
11
+ return unless query
12
+
13
+ opt_sort ||= 'name'
14
+ url = "#{SEARCH_URL}#{query}"
15
+
16
+ begin
17
+ open(url) do |f|
18
+ gems = JSON.parse(f.read)
19
+ if gems.size.zero?
20
+ puts 'We did not find results.'
21
+ return
22
+ end
23
+ fmt_size = ruled_line_size(gems)
24
+ gems_sort!(gems, opt_sort)
25
+ render_header(fmt_size)
26
+ render_body(gems, fmt_size)
27
+ end
28
+ rescue
29
+ puts 'An unexpected Network error has occurred.'
30
+ end
31
+ end
32
+
33
+ private
34
+ def ruled_line_size(gems)
35
+ line_size = DEFAULT_RULED_LINE_SIZE.dup
36
+ max_name_size = gems.map { |gem| "#{gem['name']} (#{gem['version']})".size }.max
37
+ line_size[0] = max_name_size if max_name_size > line_size[0]
38
+ line_size
39
+ end
40
+
41
+ def gems_sort!(gems, opt_sort)
42
+ if opt_sort == 'name'
43
+ gems.sort!{ |x,y| x[opt_sort] <=> y[opt_sort] }
44
+ else
45
+ gems.sort!{ |x,y| y[opt_sort] <=> x[opt_sort] }
46
+ end
47
+ end
48
+
49
+ def render_header(f)
50
+ fmt = "%-#{f[0]}s %#{f[1]}s %#{f[2]}s"
51
+ puts fmt % ['NAME', 'DL(ver)', 'DL(all)']
52
+ puts fmt % ['-'*f[0], '-'*f[1], '-'*f[2]]
53
+ end
54
+
55
+ def render_body(gems, f)
56
+ fmt = "%-#{f[0]}s %#{f[1]}d %#{f[2]}d"
57
+ gems.each do |gem|
58
+ puts fmt % [
59
+ "#{gem['name']} (#{gem['version']})",
60
+ gem['version_downloads'],
61
+ gem['downloads']
62
+ ]
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module Gem::Search
2
+ VERSION = "0.0.2"
3
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,197 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gem::Search::CLI do
4
+ describe '#search' do
5
+ before do
6
+ @cli = Gem::Search::CLI.new
7
+ stub_request(:get, build_search_uri('factory_girl')).
8
+ to_return(:status => 200, :body => dummy_search_result)
9
+ end
10
+
11
+ context 'when a network error occurred' do
12
+ before do
13
+ stub_request(:get, build_search_uri('network_error_orccurred')).
14
+ to_return(:status => 500, :body => '[]')
15
+ end
16
+ it 'should display a network error message' do
17
+ capture(:stdout) { @cli.search('network_error_orccurred') }.should == "An unexpected Network error has occurred.\n"
18
+ end
19
+ end
20
+
21
+ context 'with nonexistence gem' do
22
+ before do
23
+ stub_request(:get, build_search_uri('no_match_gem_name')).
24
+ to_return(:status => 200, :body => '[]')
25
+ end
26
+ it 'should display a nonexistence message' do
27
+ capture(:stdout) { @cli.search('no_match_gem_name') }.should == "We did not find results.\n"
28
+ end
29
+ end
30
+
31
+ context 'with existing gem' do
32
+ context 'with no sort option' do
33
+ it 'should display rubygems ordering by name' do
34
+ capture(:stdout) { @cli.search('factory_girl') }.should == <<-'EOS'
35
+ NAME DL(ver) DL(all)
36
+ -------------------------------------------------- -------- ---------
37
+ factory_girl (3.6.0) 541 2042859
38
+ factory_girl_generator (0.0.3) 8015 15547
39
+ factory_girl_rails (3.5.0) 39724 1238780
40
+ EOS
41
+ end
42
+ end
43
+
44
+ context 'with sort option: [vd]version_downloads' do
45
+ it "should display rubygems ordering by name" do
46
+ capture(:stdout) { @cli.search('factory_girl', 'version_downloads') }.should == <<'EOS'
47
+ NAME DL(ver) DL(all)
48
+ -------------------------------------------------- -------- ---------
49
+ factory_girl_rails (3.5.0) 39724 1238780
50
+ factory_girl_generator (0.0.3) 8015 15547
51
+ factory_girl (3.6.0) 541 2042859
52
+ EOS
53
+ end
54
+ end
55
+
56
+ context 'with sort option: [d]download' do
57
+ it "should display rubygems ordering by name" do
58
+ capture(:stdout) { @cli.search('factory_girl', 'download') }.should == <<'EOS'
59
+ NAME DL(ver) DL(all)
60
+ -------------------------------------------------- -------- ---------
61
+ factory_girl (3.6.0) 541 2042859
62
+ factory_girl_rails (3.5.0) 39724 1238780
63
+ factory_girl_generator (0.0.3) 8015 15547
64
+ EOS
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'ruled NAME line' do
70
+ context 'NAME size is 42' do
71
+ before do
72
+ stub_request(:get, build_search_uri('size_is_42_2345678901234567890123456789012')).
73
+ to_return(:status => 200, :body => dummy_search_result_name_size_is_42)
74
+ end
75
+ it "should be 50 characters" do
76
+ capture(:stdout) { @cli.search('size_is_42_2345678901234567890123456789012') }.should == <<'EOS'
77
+ NAME DL(ver) DL(all)
78
+ -------------------------------------------------- -------- ---------
79
+ size_is_42_2345678901234567890123456789012 (0.0.1) 100 1000
80
+ EOS
81
+ end
82
+ end
83
+
84
+ context 'NAME size is 43' do
85
+ before do
86
+ stub_request(:get, build_search_uri('size_is_42_2345678901234567890123456789012')).
87
+ to_return(:status => 200, :body => dummy_search_result_name_size_is_43)
88
+ end
89
+ it "should be 51 characters" do
90
+ capture(:stdout) { @cli.search('size_is_42_2345678901234567890123456789012') }.should == <<'EOS'
91
+ NAME DL(ver) DL(all)
92
+ --------------------------------------------------- -------- ---------
93
+ size_is_43_23456789012345678901234567890123 (0.0.2) 200 2000
94
+ EOS
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ private
101
+ def build_search_uri(query)
102
+ "#{Gem::Search::CLI::SEARCH_URL}#{query}"
103
+ end
104
+
105
+ def dummy_search_result
106
+ # top 3 gems searching with 'factory_girl'
107
+ # https://rubygems.org/api/v1/search.json?query=factory_girl
108
+ [{"name"=>"factory_girl",
109
+ "downloads"=>2042859,
110
+ "version"=>"3.6.0",
111
+ "version_downloads"=>541,
112
+ "platform"=>"ruby",
113
+ "authors"=>"Josh Clayton, Joe Ferris",
114
+ "info"=>
115
+ "factory_girl provides a framework and DSL for defining and\n using factories - less error-prone, more explicit, and\n all-around easier to work with than fixtures.",
116
+ "project_uri"=>"http://rubygems.org/gems/factory_girl",
117
+ "gem_uri"=>"http://rubygems.org/gems/factory_girl-3.6.0.gem",
118
+ "homepage_uri"=>"https://github.com/thoughtbot/factory_girl",
119
+ "wiki_uri"=>"",
120
+ "documentation_uri"=>"",
121
+ "mailing_list_uri"=>"",
122
+ "source_code_uri"=>"https://github.com/thoughtbot/factory_girl",
123
+ "bug_tracker_uri"=>"",
124
+ "dependencies"=>
125
+ {"development"=>
126
+ [{"name"=>"appraisal", "requirements"=>"~> 0.4"},
127
+ {"name"=>"aruba", "requirements"=>">= 0"},
128
+ {"name"=>"bourne", "requirements"=>">= 0"},
129
+ {"name"=>"cucumber", "requirements"=>"~> 1.1"},
130
+ {"name"=>"mocha", "requirements"=>">= 0"},
131
+ {"name"=>"rspec", "requirements"=>"~> 2.0"},
132
+ {"name"=>"simplecov", "requirements"=>">= 0"},
133
+ {"name"=>"sqlite3-ruby", "requirements"=>">= 0"},
134
+ {"name"=>"timecop", "requirements"=>">= 0"},
135
+ {"name"=>"yard", "requirements"=>">= 0"}],
136
+ "runtime"=>[{"name"=>"activesupport", "requirements"=>">= 3.0.0"}]}},
137
+ {"name"=>"factory_girl_rails",
138
+ "downloads"=>1238780,
139
+ "version"=>"3.5.0",
140
+ "version_downloads"=>39724,
141
+ "platform"=>"ruby",
142
+ "authors"=>"Joe Ferris",
143
+ "info"=>
144
+ "factory_girl_rails provides integration between\n factory_girl and rails 3 (currently just automatic factory definition\n loading)",
145
+ "project_uri"=>"http://rubygems.org/gems/factory_girl_rails",
146
+ "gem_uri"=>"http://rubygems.org/gems/factory_girl_rails-3.5.0.gem",
147
+ "homepage_uri"=>"http://github.com/thoughtbot/factory_girl_rails",
148
+ "wiki_uri"=>nil,
149
+ "documentation_uri"=>nil,
150
+ "mailing_list_uri"=>nil,
151
+ "source_code_uri"=>nil,
152
+ "bug_tracker_uri"=>nil,
153
+ "dependencies"=>
154
+ {"development"=>
155
+ [{"name"=>"aruba", "requirements"=>">= 0"},
156
+ {"name"=>"cucumber", "requirements"=>"~> 1.0.0"},
157
+ {"name"=>"rails", "requirements"=>"= 3.0.7"},
158
+ {"name"=>"rake", "requirements"=>">= 0"},
159
+ {"name"=>"rspec", "requirements"=>"~> 2.6.0"}],
160
+ "runtime"=>
161
+ [{"name"=>"factory_girl", "requirements"=>"~> 3.5.0"},
162
+ {"name"=>"railties", "requirements"=>">= 3.0.0"}]}},
163
+ {"name"=>"factory_girl_generator",
164
+ "downloads"=>15547,
165
+ "version"=>"0.0.3",
166
+ "version_downloads"=>8015,
167
+ "platform"=>"ruby",
168
+ "authors"=>"Les Hill",
169
+ "info"=>"Rails 3 generator for factory_girl",
170
+ "project_uri"=>"http://rubygems.org/gems/factory_girl_generator",
171
+ "gem_uri"=>"http://rubygems.org/gems/factory_girl_generator-0.0.3.gem",
172
+ "homepage_uri"=>"http://github.com/leshill/factory_girl_generator",
173
+ "wiki_uri"=>nil,
174
+ "documentation_uri"=>nil,
175
+ "mailing_list_uri"=>nil,
176
+ "source_code_uri"=>nil,
177
+ "bug_tracker_uri"=>nil,
178
+ "dependencies"=>{"development"=>[], "runtime"=>[]}}
179
+ ].to_json
180
+ end
181
+
182
+ def dummy_search_result_name_size_is_42
183
+ [{"name"=>"size_is_42_2345678901234567890123456789012",
184
+ "downloads"=>1000,
185
+ "version"=>"0.0.1",
186
+ "version_downloads"=>100}
187
+ ].to_json
188
+ end
189
+
190
+ def dummy_search_result_name_size_is_43
191
+ [{"name"=>"size_is_43_23456789012345678901234567890123",
192
+ "downloads"=>2000,
193
+ "version"=>"0.0.2",
194
+ "version_downloads"=>200}
195
+ ].to_json
196
+ end
197
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'webmock/rspec'
4
+ require 'gem-search'
5
+
6
+ # http://d.hatena.ne.jp/POCHI_BLACK/20100324
7
+ # this method is written by wycats
8
+ def capture(stream)
9
+ begin
10
+ stream = stream.to_s
11
+ eval "$#{stream} = StringIO.new"
12
+ yield
13
+ result = eval("$#{stream}").string
14
+ ensure
15
+ eval("$#{stream} = #{stream.upcase}")
16
+ end
17
+
18
+ result
19
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gem-search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - rochefort
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: slop
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: webmock
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: search gems with using rubygems.org API
63
+ email:
64
+ - terasawan@gmail.com
65
+ executables:
66
+ - gem-search
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - Gemfile
72
+ - LICENSE
73
+ - README.md
74
+ - Rakefile
75
+ - bin/gem-search
76
+ - gem-search.gemspec
77
+ - lib/gem-search.rb
78
+ - lib/gem-search/cli.rb
79
+ - lib/gem-search/version.rb
80
+ - spec/cli_spec.rb
81
+ - spec/spec_helper.rb
82
+ homepage: https://github.com/rochefort/gem-search
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 1.8.23
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: search gems with using rubygems.org API
106
+ test_files:
107
+ - spec/cli_spec.rb
108
+ - spec/spec_helper.rb