stackoverflow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/stackoverflow.rb +178 -0
- metadata +64 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'json'
|
7
|
+
require 'nokogiri'
|
8
|
+
require 'terminal-table/import'
|
9
|
+
require 'sqlite3'
|
10
|
+
require 'optparse'
|
11
|
+
|
12
|
+
|
13
|
+
module StackOverflow
|
14
|
+
class API
|
15
|
+
def search(search_string)
|
16
|
+
search_string = URI::encode(search_string)
|
17
|
+
api_get("/2.0/similar?order=desc&sort=votes&title=#{search_string}&site=stackoverflow&filter=!9Tk5iz1Gf")
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_question(question_id)
|
21
|
+
api_get("/2.0/questions/#{question_id}?order=desc&sort=activity&site=stackoverflow&filter=!9Tk5izFWA")
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_answers(question_id)
|
25
|
+
api_get("/2.0/questions/#{question_id}/answers?order=desc&sort=votes&site=stackoverflow&filter=!9Tk6JYC_e")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def api_get(path)
|
31
|
+
url = "https://api.stackexchange.com" + path
|
32
|
+
u = URI.parse(url)
|
33
|
+
Net::HTTP.start(u.host, u.port, :use_ssl => true) do |http|
|
34
|
+
response = http.get(u.request_uri)
|
35
|
+
return JSON(response.body)['items']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class DB
|
41
|
+
def initialize
|
42
|
+
@db = SQLite3::Database.new("../data/stackoverflow.db")
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(search_string)
|
46
|
+
sql = "SELECT id, score, body, title FROM posts WHERE post_type_id=1 AND "
|
47
|
+
sub_sql = []
|
48
|
+
for search_term in search_string.split(' ')
|
49
|
+
sub_sql << "title LIKE '%#{search_term}%"
|
50
|
+
end
|
51
|
+
sql += sub_sql.join(' AND ')
|
52
|
+
sql += ' ORDER BY score DESC'
|
53
|
+
|
54
|
+
questions = []
|
55
|
+
@db.execute(sql) do |row|
|
56
|
+
questions << { :id => row[0],
|
57
|
+
:score => row[1],
|
58
|
+
:body => row[2],
|
59
|
+
:title => row[3],
|
60
|
+
:link => '',
|
61
|
+
:answers => get_answers(row[0]) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_answers(question_id)
|
66
|
+
answers = []
|
67
|
+
sql = "SELECT id, score, body FROM posts WHERE post_type_id=2 AND parent_id=#{question_id} ORDER BY score DESC"
|
68
|
+
@db.execute(sql) do |row|
|
69
|
+
answers << { :id => row[0],
|
70
|
+
:score => row[1],
|
71
|
+
:body => row[2] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Formatter
|
77
|
+
def questions_list(questions)
|
78
|
+
nb = 1
|
79
|
+
|
80
|
+
table = Terminal::Table.new do |t|
|
81
|
+
questions.each do |question|
|
82
|
+
score = question['score'] > 0 ? "+#{question['score']}" : question['score']
|
83
|
+
t << ["[#{nb}]", "(#{score})", question['title']]
|
84
|
+
nb += 1
|
85
|
+
end
|
86
|
+
t.style = {:padding_left => 2, :border_x => " ", :border_i => " ", :border_y => " "}
|
87
|
+
end
|
88
|
+
puts table
|
89
|
+
end
|
90
|
+
|
91
|
+
def question_viewer(question)
|
92
|
+
answers = question['answers']
|
93
|
+
nb = 1
|
94
|
+
|
95
|
+
table = Terminal::Table.new do |t|
|
96
|
+
t << ["Question", html2text(question['body'])]
|
97
|
+
t.title = question['title'] + "\n\n" + question['link']
|
98
|
+
t << :separator
|
99
|
+
t << :separator
|
100
|
+
|
101
|
+
answers.each do |answer|
|
102
|
+
text = html2text(answer['body'])
|
103
|
+
t << ["[#{nb}] (+#{answer['score']})", "#{text}"]
|
104
|
+
t << :separator
|
105
|
+
nb += 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
IO.popen("less", "w") { |f| f.puts table }
|
110
|
+
end
|
111
|
+
|
112
|
+
def html2text(html)
|
113
|
+
doc = Nokogiri::HTML(html)
|
114
|
+
doc = doc.css('body').text.squeeze(" ").squeeze("\n").gsub(/[\n]+/, "\n\n")
|
115
|
+
wordwrap(doc)
|
116
|
+
end
|
117
|
+
|
118
|
+
def wordwrap(str, columns=80)
|
119
|
+
str.gsub(/\t/, " ").gsub(/.{1,#{ columns }}(?:\s|\Z)/) do
|
120
|
+
($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Command
|
126
|
+
def run
|
127
|
+
options = {}
|
128
|
+
OptionParser.new do |opts|
|
129
|
+
opts.banner = "Usage: so [options] <search string> [<question_id>]"
|
130
|
+
opts.on("-h", "--help", "Help") do |v|
|
131
|
+
help(opts)
|
132
|
+
end
|
133
|
+
opts.on("-o", "--offline", "Offline mode") do |v|
|
134
|
+
options['offline'] = true
|
135
|
+
end
|
136
|
+
end.parse!
|
137
|
+
|
138
|
+
# last argument is integer when user is specifing a question_nb from the results
|
139
|
+
question_nb = nil
|
140
|
+
if ARGV[-1] =~ /^[0-9]+$/
|
141
|
+
question_nb = ARGV.pop.to_i
|
142
|
+
end
|
143
|
+
|
144
|
+
search_string = ARGV.join(' ')
|
145
|
+
|
146
|
+
search(search_string, question_nb, options)
|
147
|
+
end
|
148
|
+
|
149
|
+
def help(opts)
|
150
|
+
puts opts.banner + "\n\n"
|
151
|
+
puts "\t<search_string>: Search Stack Overflow for a combination of words"
|
152
|
+
puts "\t<question_id>: (Optional) Display the question with this #id from the search results"
|
153
|
+
end
|
154
|
+
|
155
|
+
def search(search_string, question_nb, options)
|
156
|
+
if options['offline']
|
157
|
+
api = API.new
|
158
|
+
#api = DB.new
|
159
|
+
else
|
160
|
+
api = API.new
|
161
|
+
end
|
162
|
+
|
163
|
+
questions = api.search(search_string)
|
164
|
+
if !questions or questions.length == 0
|
165
|
+
puts "No record found - Try a less specific (or sometimes, more specific) query"
|
166
|
+
return
|
167
|
+
end
|
168
|
+
|
169
|
+
if !question_nb
|
170
|
+
Formatter.new.questions_list(questions)
|
171
|
+
else
|
172
|
+
question = questions[question_nb.to_i - 1]
|
173
|
+
Formatter.new.question_viewer(question)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stackoverflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Xavier Antoviaque
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-24 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Query StackOverflow from the command line (offline/online modes)
|
22
|
+
email: xavier@antoviaque.org
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/stackoverflow.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: https://github.com/antoviaque/stack-overflow-command-line
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.3.7
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: Query StackOverflow from the command line (offline/online modes)
|
63
|
+
test_files: []
|
64
|
+
|