noms-command 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE +191 -0
- data/README.rst +376 -0
- data/ROADMAP.rst +127 -0
- data/Rakefile +49 -0
- data/TODO.rst +7 -0
- data/bin/noms2 +20 -0
- data/fixture/dnc.rb +120 -0
- data/fixture/identity +5 -0
- data/fixture/public/dnc.json +22 -0
- data/fixture/public/echo.json +7 -0
- data/fixture/public/files/data.json +12 -0
- data/fixture/public/files/foo.json +2 -0
- data/fixture/public/lib/dnc.js +81 -0
- data/fixture/public/lib/noms-args.js +13 -0
- data/fixture/public/lib/showopt.js +18 -0
- data/fixture/public/location.json +8 -0
- data/fixture/public/showopt.json +15 -0
- data/fixture/rig2json +21 -0
- data/lib/noms/command/application.rb +204 -0
- data/lib/noms/command/auth/identity.rb +62 -0
- data/lib/noms/command/auth.rb +117 -0
- data/lib/noms/command/base.rb +22 -0
- data/lib/noms/command/document.rb +59 -0
- data/lib/noms/command/error.rb +11 -0
- data/lib/noms/command/formatter.rb +178 -0
- data/lib/noms/command/urinion/data.rb +63 -0
- data/lib/noms/command/urinion.rb +29 -0
- data/lib/noms/command/useragent.rb +134 -0
- data/lib/noms/command/version.rb +7 -0
- data/lib/noms/command/window.rb +95 -0
- data/lib/noms/command/xmlhttprequest.rb +181 -0
- data/lib/noms/command.rb +107 -0
- data/noms-command.gemspec +30 -0
- data/spec/01noms-command_spec.rb +30 -0
- data/spec/02noms-command.sh +31 -0
- data/spec/03application_spec.rb +47 -0
- data/spec/04application_spec.rb +61 -0
- data/spec/05formatter_spec.rb +195 -0
- data/spec/06urinion_data.rb +20 -0
- data/spec/07js_spec.rb +87 -0
- data/spec/08xhr_spec.rb +209 -0
- data/spec/09bookmarks_spec.rb +60 -0
- data/spec/10auth_spec.rb +33 -0
- data/spec/spec_helper.rb +40 -0
- metadata +228 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
class NOMS
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class NOMS::Command
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command::URInion
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class NOMS::Command::URInion::Data
|
18
|
+
|
19
|
+
attr_accessor :scheme, :host, :port, :fragment, :query, :data, :path,
|
20
|
+
:data_encoding, :mime_type, :user, :password, :raw_data,
|
21
|
+
:character_set
|
22
|
+
|
23
|
+
def self.parse(urltext)
|
24
|
+
self.new(urltext)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(urltext)
|
28
|
+
@host = nil
|
29
|
+
@port = nil
|
30
|
+
@fragment = nil
|
31
|
+
@user = nil
|
32
|
+
@path = nil
|
33
|
+
@password = nil
|
34
|
+
@character_set = nil
|
35
|
+
|
36
|
+
@scheme, rest = urltext.split(':', 2)
|
37
|
+
raise NOMS::Command::Error.new "URL is not data: (scheme = #{@scheme})" unless @scheme == 'data'
|
38
|
+
meta, @raw_data = rest.split(',', 2)
|
39
|
+
fields = meta.split(';')
|
40
|
+
if fields[-1] == 'base64'
|
41
|
+
@data_encoding = fields.pop
|
42
|
+
end
|
43
|
+
|
44
|
+
unless fields[0].nil? or fields[0].empty?
|
45
|
+
@mime_type = fields.shift
|
46
|
+
end
|
47
|
+
|
48
|
+
fields.each do |field|
|
49
|
+
if m = /charset=(.*)/.match(field)
|
50
|
+
@character_set = m[1]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
case @data_encoding
|
55
|
+
when 'base64'
|
56
|
+
@data = Base64.urlsafe_decode64
|
57
|
+
else
|
58
|
+
@data = @raw_data
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'noms/command/urinion/data'
|
5
|
+
|
6
|
+
class NOMS
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
class NOMS::Command
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
class NOMS::Command::URInion
|
15
|
+
|
16
|
+
def self.parse(url)
|
17
|
+
if url.respond_to? :scheme
|
18
|
+
url
|
19
|
+
else
|
20
|
+
case url
|
21
|
+
when /^data/
|
22
|
+
NOMS::Command::URInion::Data.parse(url)
|
23
|
+
else
|
24
|
+
URI.parse(url)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'openssl'
|
6
|
+
require 'httpclient'
|
7
|
+
require 'uri'
|
8
|
+
require 'highline/import'
|
9
|
+
|
10
|
+
require 'noms/command/auth'
|
11
|
+
require 'noms/command/base'
|
12
|
+
|
13
|
+
class NOMS
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class NOMS::Command
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class NOMS::Command::UserAgent < NOMS::Command::Base
|
22
|
+
|
23
|
+
def initialize(origin, attrs={})
|
24
|
+
@origin = origin
|
25
|
+
@client = HTTPClient.new :agent_name => "noms/#{NOMS::Command::VERSION}"
|
26
|
+
# TODO Replace with TOFU implementation
|
27
|
+
@client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
28
|
+
@redirect_checks = [ ]
|
29
|
+
@log = attrs[:logger] || default_logger
|
30
|
+
|
31
|
+
@log.debug "(UserAgent) specified identities = #{attrs[:specified_identities]}"
|
32
|
+
@auth = NOMS::Command::Auth.new(:logger => @log,
|
33
|
+
:specified_identities => (attrs[:specified_identities] || []))
|
34
|
+
# TODO: Set cookie jar to something origin-specific
|
35
|
+
# TODO: Set user-agent to something nomsy
|
36
|
+
# caching
|
37
|
+
@client.redirect_uri_callback = lambda do |uri, res|
|
38
|
+
raise NOMS::Command::Error.new "Bad redirect URL #{url}" unless check_redirect(uri)
|
39
|
+
@client.default_redirect_uri_callback(uri, res)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def auth
|
44
|
+
@auth
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_redirect(url)
|
48
|
+
@log.debug "Running #{@redirect_checks.size} redirect checks on #{url}" unless @redirect_checks.empty?
|
49
|
+
@redirect_checks.all? { |check| check.call(url) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def origin=(new_origin)
|
53
|
+
@log.debug "Setting my origin to #{new_origin}"
|
54
|
+
@origin = new_origin
|
55
|
+
end
|
56
|
+
|
57
|
+
def absolute_url(url)
|
58
|
+
@log.debug "Calculating absolute url of #{url} in context of #{@origin}"
|
59
|
+
begin
|
60
|
+
url = URI.parse url unless url.respond_to? :scheme
|
61
|
+
url = URI.join(@origin, url) unless url.absolute?
|
62
|
+
url
|
63
|
+
rescue StandardError => e
|
64
|
+
raise NOMS::Command::Error.new "Error parsing URL #{url} in context of #{@origin} (#{e.class}): #{e.message}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def request(method, url, data=nil, headers={}, tries=10, identity=nil)
|
69
|
+
req_url = absolute_url(url)
|
70
|
+
@log.debug "#{method} #{req_url}" + (headers.empty? ? '' : headers.inspect)
|
71
|
+
response = @client.request(method.to_s.upcase, req_url, '', data, headers)
|
72
|
+
@log.debug "-> #{response.status} #{response.reason} (#{response.content.size} bytes of #{response.contenttype})"
|
73
|
+
@log.debug JSON.pretty_generate(response.headers)
|
74
|
+
case response.status
|
75
|
+
when 401
|
76
|
+
@log.debug " handling unauthorized"
|
77
|
+
if identity
|
78
|
+
@log.debug " we have an identity #{identity} but are trying again"
|
79
|
+
if tries > 0
|
80
|
+
@log.debug "loading authentication identity for #{url}"
|
81
|
+
identity = @auth.load(url, response)
|
82
|
+
@client.set_auth(identity['domain'], identity['username'], identity['password'])
|
83
|
+
response, req_url = self.request(method, url, data, headers, tries - 1, identity)
|
84
|
+
end
|
85
|
+
else
|
86
|
+
identity = @auth.load(url, response)
|
87
|
+
@client.set_auth(identity['domain'], identity['username'], identity['password'])
|
88
|
+
response, req_url = self.request(method, url, data, headers, 2, identity)
|
89
|
+
end
|
90
|
+
when 302, 301
|
91
|
+
new_url = response.header['location'].first
|
92
|
+
if check_redirect new_url
|
93
|
+
@log.debug "redirect to #{new_url}"
|
94
|
+
raise NOMS::Command::Error.new "Can't follow redirect to #{new_url}: too many redirects" if tries <= 0
|
95
|
+
response, req_url = self.request(method, new_url, data, headers, tries - 1)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
if identity and response.ok?
|
100
|
+
@log.debug "Login succeeded, saving #{identity}"
|
101
|
+
identity.save
|
102
|
+
end
|
103
|
+
|
104
|
+
@log.debug "<- #{response.status} #{response.reason} <- #{req_url}"
|
105
|
+
[response, req_url]
|
106
|
+
end
|
107
|
+
|
108
|
+
def get(url, headers={})
|
109
|
+
request('GET', url, nil, headers)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Wait for all asynchronous requests to complete.
|
113
|
+
# A stub while these are simulated
|
114
|
+
def wait(on=nil)
|
115
|
+
[]
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_redirect_check(&block)
|
119
|
+
@log.debug "Adding #{block} to redirect checks"
|
120
|
+
@redirect_checks << block
|
121
|
+
end
|
122
|
+
|
123
|
+
def clear_redirect_checks
|
124
|
+
@log.debug "Clearing redirect checks"
|
125
|
+
@redirect_checks = [ ]
|
126
|
+
end
|
127
|
+
|
128
|
+
def pop_redirect_check
|
129
|
+
unless @redirect_checks.empty?
|
130
|
+
@log.debug "Popping redirect check: #{@redirect_checks[-1]}"
|
131
|
+
@redirect_checks.pop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'highline/import'
|
6
|
+
|
7
|
+
require 'noms/command/base'
|
8
|
+
|
9
|
+
class NOMS
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
class NOMS::Command::Window < NOMS::Command::Base
|
18
|
+
|
19
|
+
attr_accessor :document, :name, :origin
|
20
|
+
attr_reader :console, :origin, :location
|
21
|
+
|
22
|
+
def initialize(invoker=$0, origin=nil, opt={})
|
23
|
+
@document = nil
|
24
|
+
@origin = origin
|
25
|
+
@name = invoker
|
26
|
+
@log = opt[:logger] || default_logger
|
27
|
+
@console = NOMS::Command::Window::Console.new(@log)
|
28
|
+
@location = NOMS::Command::Window::Location.new(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def isatty()
|
32
|
+
$stdout.tty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def alert(msg)
|
36
|
+
@log.error msg
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt(prompt_text, echo=true)
|
40
|
+
echo = true if echo.nil?
|
41
|
+
@log.debug "prompt(#{prompt_text.inspect}, #{echo.inspect})"
|
42
|
+
v = ask(prompt_text) { |p| p.echo = false unless echo }
|
43
|
+
@log.debug "-> #{v}"
|
44
|
+
v
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class NOMS::Command::Window::Console < NOMS::Command::Base
|
50
|
+
|
51
|
+
def initialize(logger=nil)
|
52
|
+
@log = logger || default_logger
|
53
|
+
end
|
54
|
+
|
55
|
+
# Some implementations have a kind of format string. I don't
|
56
|
+
def log(*items)
|
57
|
+
@log.debug(items.map { |s| _sanitize(s) }.join(', '))
|
58
|
+
end
|
59
|
+
|
60
|
+
def _sanitize(s)
|
61
|
+
s.respond_to?(:to_str) ? s : s.to_json
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
class NOMS::Command::Window::Location
|
67
|
+
|
68
|
+
{ :protocol => :scheme,
|
69
|
+
:origin => :to_s,
|
70
|
+
:pathname => :path,
|
71
|
+
:href => :to_s,
|
72
|
+
:hostname => :host,
|
73
|
+
:hash => :fragment,
|
74
|
+
:search => :query,
|
75
|
+
:port => :port
|
76
|
+
}.each do |loc_attr, uri_attr|
|
77
|
+
define_method(loc_attr) do
|
78
|
+
self.field uri_attr
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(window)
|
83
|
+
@window = window
|
84
|
+
# Setup simple equivalences
|
85
|
+
end
|
86
|
+
|
87
|
+
def field(name)
|
88
|
+
@window.origin.send(name) if @window.origin.respond_to? name
|
89
|
+
end
|
90
|
+
|
91
|
+
def host
|
92
|
+
[self.hostname, self.port].join(':')
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
#!/usr/bin/env
|
2
|
+
|
3
|
+
require 'noms/command/urinion'
|
4
|
+
require 'noms/command/useragent'
|
5
|
+
require 'noms/command/error'
|
6
|
+
|
7
|
+
require 'httpclient'
|
8
|
+
|
9
|
+
class NOMS
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class NOMS::Command
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
# Implements a subset of the XMLHttpRequest interface. This
|
18
|
+
# class is exposed as a constructor function in the Javascript
|
19
|
+
# VM. It uses NOMS::Command::UserAgent for HTTP conversations,
|
20
|
+
# adding checks for the same-origin policy.
|
21
|
+
class NOMS::Command::XMLHttpRequest
|
22
|
+
|
23
|
+
OPENED = 1
|
24
|
+
HEADERS_RECEIVED = 2
|
25
|
+
LOADING = 3
|
26
|
+
DONE = 4
|
27
|
+
|
28
|
+
@@origin = nil
|
29
|
+
@@ua = nil
|
30
|
+
|
31
|
+
def self.origin
|
32
|
+
@@origin
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.origin=(origin)
|
36
|
+
@@origin = NOMS::Command::URInion.parse(origin)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.useragent
|
40
|
+
@@ua
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the class attribute for the UserAgent; ideally
|
44
|
+
# this is shared among everything in one *noms*
|
45
|
+
# invocation.
|
46
|
+
def self.useragent=(ua)
|
47
|
+
@@ua = ua
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_accessor :responseText, :headers
|
51
|
+
|
52
|
+
def initialize()
|
53
|
+
@origin = @@origin
|
54
|
+
@ua = @@ua || NOMS::Command::UserAgent.new(@origin)
|
55
|
+
@headers = { }
|
56
|
+
@readyState = 0
|
57
|
+
end
|
58
|
+
|
59
|
+
def useragent
|
60
|
+
@ua
|
61
|
+
end
|
62
|
+
|
63
|
+
# Checks whether the URL has the same origin
|
64
|
+
# as the "origin" URL
|
65
|
+
def same_origin?(other)
|
66
|
+
other = NOMS::Command::URInion.parse(other)
|
67
|
+
return false unless @origin.scheme == other.scheme
|
68
|
+
return false unless @origin.host == other.host
|
69
|
+
return false unless @origin.port == other.port
|
70
|
+
return true
|
71
|
+
end
|
72
|
+
|
73
|
+
def open(method, url, async=true, user=nil, password=nil)
|
74
|
+
raise NOMS::Command::Error.new "origin of #{url} doesn't match application origin (#{@origin})" unless
|
75
|
+
same_origin? @ua.absolute_url(url)
|
76
|
+
# Should we run onreadystatechange when resetting this? Not doing it for now.
|
77
|
+
@readyState = 0
|
78
|
+
@responseText = ''
|
79
|
+
@response = nil
|
80
|
+
@method = method
|
81
|
+
@url = url
|
82
|
+
@async = async
|
83
|
+
@user = user
|
84
|
+
@password = password
|
85
|
+
end
|
86
|
+
|
87
|
+
def onreadystatechange
|
88
|
+
@onreadystatechange
|
89
|
+
end
|
90
|
+
|
91
|
+
def onreadystatechange=(callback)
|
92
|
+
@onreadystatechange = callback
|
93
|
+
end
|
94
|
+
|
95
|
+
def readyState
|
96
|
+
@readyState
|
97
|
+
end
|
98
|
+
|
99
|
+
def readyState=(value)
|
100
|
+
@readyState = value
|
101
|
+
unless @onreadystatechange.nil?
|
102
|
+
@onreadystatechange.methodcall self
|
103
|
+
end
|
104
|
+
@readyState
|
105
|
+
end
|
106
|
+
|
107
|
+
def setRequestHeader(header, value)
|
108
|
+
@headers[header] = value
|
109
|
+
end
|
110
|
+
|
111
|
+
def OPENED
|
112
|
+
OPENED
|
113
|
+
end
|
114
|
+
|
115
|
+
def HEADERS_RECEIVED
|
116
|
+
HEADERS_RECEIVED
|
117
|
+
end
|
118
|
+
|
119
|
+
def LOADING
|
120
|
+
LOADING
|
121
|
+
end
|
122
|
+
|
123
|
+
def DONE
|
124
|
+
DONE
|
125
|
+
end
|
126
|
+
|
127
|
+
# This problematic implementations attempts to "guess"
|
128
|
+
# when Object#send is intended, and when the Javascript-
|
129
|
+
# style xhr.send() is desired. The one ambiguous situation
|
130
|
+
# it can't recover from is when there is one string argument,
|
131
|
+
# this is a valid invocation of XMLHttpRequest#send as well
|
132
|
+
# as Object#send, and we settle on XMLHttpRequest#send.
|
133
|
+
def send(*vary)
|
134
|
+
if vary.size == 0 or (vary.size == 1 and ! vary[0].is_a? Symbol)
|
135
|
+
do_send(*vary)
|
136
|
+
else
|
137
|
+
super
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# NOMS::Command::UserAgent doesn't do async
|
142
|
+
# calls (yet) since httpclient doesn't do
|
143
|
+
# anything special with them and you can
|
144
|
+
# only busy-wait on them. So they're "simulated",
|
145
|
+
# and by "simulated" I mean "performed synchronously".
|
146
|
+
def do_send(data=nil)
|
147
|
+
# @async ignored
|
148
|
+
@ua.add_redirect_check do |url|
|
149
|
+
self.same_origin? url
|
150
|
+
end
|
151
|
+
@response, landing_url = @ua.request(@method, @url, data, @headers)
|
152
|
+
# We don't need the 'landing' URL
|
153
|
+
@ua.pop_redirect_check
|
154
|
+
self.readyState = OPENED
|
155
|
+
self.readyState = HEADERS_RECEIVED
|
156
|
+
self.readyState = LOADING
|
157
|
+
@responseText = @response.content
|
158
|
+
self.readyState = DONE
|
159
|
+
end
|
160
|
+
|
161
|
+
def status
|
162
|
+
@response.status.to_i unless @response.nil?
|
163
|
+
end
|
164
|
+
|
165
|
+
def statusText
|
166
|
+
@response.status + ' ' + @response.reason unless @response.nil?
|
167
|
+
end
|
168
|
+
|
169
|
+
def getResponseHeader(header)
|
170
|
+
@response.header[header.downcase] unless @response.nil?
|
171
|
+
end
|
172
|
+
|
173
|
+
def getAllResponseHeaders
|
174
|
+
lambda { || @response.headers.map { |h, v| "#{h}: #{v}" }.join("\n") + "\n" }
|
175
|
+
end
|
176
|
+
|
177
|
+
def abort()
|
178
|
+
lambda { || @readyState = 0 }
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
data/lib/noms/command.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'trollop'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
require 'noms/command/window'
|
9
|
+
require 'noms/command/application'
|
10
|
+
require 'noms/command/formatter'
|
11
|
+
|
12
|
+
class NOMS
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class NOMS::Command
|
17
|
+
|
18
|
+
def self.run(argv)
|
19
|
+
runner = self.new(argv)
|
20
|
+
runner.run
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(argv)
|
24
|
+
@argv = argv
|
25
|
+
@log = Logger.new($stderr)
|
26
|
+
@log.formatter = lambda { |sev, timestamp, prog, msg| msg[-1].chr == "\n" ? msg : msg + "\n" }
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
# Find my own configuration for bookmarks and stuff
|
31
|
+
# Interpret either environment variables or (maybe) initial options
|
32
|
+
parser = Trollop::Parser.new do
|
33
|
+
version NOMS::Command::VERSION
|
34
|
+
banner <<-USAGE.gsub(/^\s{16}/,'')
|
35
|
+
Usage:
|
36
|
+
noms [noms-options] { bookmark | url } [options] [arguments]
|
37
|
+
noms-options:
|
38
|
+
USAGE
|
39
|
+
opt :identity, "Identity file", :type => :string, :multi => true
|
40
|
+
opt :verbose, "Enable verbose output"
|
41
|
+
opt :list, "List bookmarks"
|
42
|
+
opt :bookmarks, "Bookmark file location (can be specified multiple times)",
|
43
|
+
:type => :string,
|
44
|
+
:multi => true
|
45
|
+
opt :nodefault_bookmarks, "Don't consult default bookmarks files",
|
46
|
+
:short => '-X',
|
47
|
+
:long => '--nodefault-bookmarks'
|
48
|
+
opt :debug, "Enable debug output"
|
49
|
+
stop_on_unknown
|
50
|
+
end
|
51
|
+
|
52
|
+
@opt = Trollop::with_standard_exception_handling parser do
|
53
|
+
parser.parse(@argv)
|
54
|
+
end
|
55
|
+
|
56
|
+
Trollop::with_standard_exception_handling parser do
|
57
|
+
raise Trollop::HelpNeeded if @argv.empty? and ! @opt[:list]
|
58
|
+
end
|
59
|
+
|
60
|
+
@opt[:debug] = true if ENV['NOMS_DEBUG'] and ! ENV['NOMS_DEBUG'].empty?
|
61
|
+
|
62
|
+
default_bookmarks =
|
63
|
+
[ File.join(ENV['HOME'], '.noms/bookmarks.json'),
|
64
|
+
'/usr/local/etc/noms/bookmarks.json',
|
65
|
+
'/etc/noms/bookmarks.json'].select { |f| File.exist? f }
|
66
|
+
|
67
|
+
@opt[:bookmarks].concat default_bookmarks unless @opt[:nodefault_bookmarks]
|
68
|
+
|
69
|
+
@log.level = Logger::WARN
|
70
|
+
@log.level = Logger::INFO if @opt[:verbose]
|
71
|
+
@log.level = Logger::DEBUG if @opt[:debug]
|
72
|
+
|
73
|
+
@bookmark = @opt[:bookmarks].map do |file|
|
74
|
+
begin
|
75
|
+
File.open(file, 'r') { |fh| JSON.load fh }
|
76
|
+
rescue JSON::ParserError => j
|
77
|
+
@log.warn "Couldn't load bookmarks from invalid JSON file #{file.inspect}"
|
78
|
+
@log.debug "JSON error: #{file.inspect}:#{j.message}"
|
79
|
+
nil
|
80
|
+
rescue StandardError => e
|
81
|
+
@log.warn "Couldn't open #{file.inspect} (#{e.class}): #{e.message}"
|
82
|
+
@log.debug "Error opening #{file.inspect}: #{e.backtrace.join("\n")}"
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end.compact.reverse.inject({}) { |h1, h2| h1.merge h2 }
|
86
|
+
|
87
|
+
if @opt[:list]
|
88
|
+
puts @bookmark.map { |pair| '%-15s -> %s' % pair }
|
89
|
+
return 0
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
origin = @bookmark[@argv[0].split('/').first] || @argv[0]
|
94
|
+
app = NOMS::Command::Application.new(origin, @argv, :logger => @log,
|
95
|
+
:specified_identities => @opt[:identity])
|
96
|
+
app.fetch! # Retrieve page
|
97
|
+
app.render! # Run scripts
|
98
|
+
out = app.display
|
99
|
+
puts out unless out.empty?
|
100
|
+
app.exitcode # Return exitcode
|
101
|
+
rescue NOMS::Command::Error => e
|
102
|
+
@log.error "noms error: #{e.message}"
|
103
|
+
255
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'noms/command/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "noms-command"
|
8
|
+
spec.version = NOMS::Command::VERSION
|
9
|
+
spec.authors = ["Jeremy Brinkley"]
|
10
|
+
spec.email = ["jbrinkley@evernote.com"]
|
11
|
+
spec.summary = %q{Interpreter for server-defined command-line interfaces}
|
12
|
+
spec.homepage = "http://github.com/en-jbrinkley/noms-command"
|
13
|
+
spec.license = "Apache-2"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "therubyracer"
|
21
|
+
spec.add_runtime_dependency "mime-types"
|
22
|
+
spec.add_runtime_dependency "httpclient"
|
23
|
+
spec.add_runtime_dependency "json"
|
24
|
+
spec.add_runtime_dependency "trollop"
|
25
|
+
spec.add_runtime_dependency "highline"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "sinatra"
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
require 'noms/command'
|
6
|
+
|
7
|
+
describe NOMS::Command do
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
setup_fixture 'test'
|
11
|
+
File.open('test/foo.txt', 'w') do |fh|
|
12
|
+
fh << <<-TEXT.gsub(/^\s+/,'')
|
13
|
+
1: Test output
|
14
|
+
2: from foo.txt
|
15
|
+
TEXT
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:all) { teardown_fixture 'test' }
|
20
|
+
|
21
|
+
describe '.run' do
|
22
|
+
context 'with one file argument' do
|
23
|
+
it 'shows the file contents' do
|
24
|
+
file = 'file://' + File.join(Dir.pwd, 'test', 'foo.txt')
|
25
|
+
expect { NOMS::Command.run([file]) }.to output(/from foo.txt/).to_stdout
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|