grimen-shelly 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.textile +109 -0
- data/Rakefile +48 -0
- data/TODO.textile +10 -0
- data/bin/shelly +5 -0
- data/config/shelly.yml +6 -0
- data/lib/shelly.rb +169 -0
- data/lib/shelly/script_source/base.rb +62 -0
- data/lib/shelly/script_source/plain/base.rb +17 -0
- data/lib/shelly/script_source/plain/gist.rb +19 -0
- data/lib/shelly/script_source/plain/pastebin.rb +19 -0
- data/lib/shelly/script_source/plain/pastie.rb +19 -0
- data/lib/shelly/script_source/plain/raw.rb +21 -0
- data/lib/shelly/script_source/scm/base.rb +11 -0
- data/lib/shelly/script_source/scm/github.rb +41 -0
- data/lib/shelly/shell_script.rb +83 -0
- data/lib/shelly/store/alias.rb +44 -0
- data/lib/shelly/store/base.rb +43 -0
- data/lib/shelly/store/repo.rb +62 -0
- data/test/shelly_test.rb +67 -0
- metadata +73 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jonas Grimfelt
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
h1. SHELLY
|
2
|
+
|
3
|
+
_Remotely stored shell scripts executable locally._
|
4
|
+
|
5
|
+
h2. The Problem
|
6
|
+
|
7
|
+
There are plenty of tools out there that makes it more or less straightforward to run scripts on a remote machine. What's funny though is that many people tend to spend more time on trying to get libraries to work on the local machine, because this is the place where the development and testing takes place. The reason for this is that compiling 3rd party libraries can be a real pain when it comes to different hardware architecture, OSs, OS versions, etc. No, package managers (such as port, apt-get, etc.) is not solving this all the way - sometimes not at all. What I usually do to in such cases is to find and copy-paste shell script "setup recipes" from docs/forums/blogs that - well, usually - works for a specific setup. This is not very scalable though, and actually a waste in many senses:
|
8
|
+
|
9
|
+
*Key reasons:*
|
10
|
+
|
11
|
+
* Old-school sharing => Setup recipes that works are spread thorough blogs/forums mostly, there's no single resource if you not having your own repository and cloning the scripts manually.
|
12
|
+
* No versioning => Same 'ol scripts might stop being "the way of doing it" because of library changes
|
13
|
+
* New machine => Same solution probably not works now, i.e. back to finding new setup recipes
|
14
|
+
* Conventions?
|
15
|
+
|
16
|
+
h2. A Solution: Shelly
|
17
|
+
|
18
|
+
Shelly solves this by letting you run scripts - on your local machine - from a remote server like local shell commands almost. You don't need to get paid storage for this; Shelly can load scripts from code hosts solutions like GitHub, Gist, Pastie, etc., which is usually used for purposes of sharing code/snippets with each other anyway.
|
19
|
+
|
20
|
+
*Key concepts:*
|
21
|
+
|
22
|
+
* Run remotely stored setup scripts/recipes on local machine directly - without downloading/copying stuff manually.
|
23
|
+
* No new DSL, just a command that runs a shell script resource (Note: No Windows scripting support).
|
24
|
+
* Easy to share setup recipes (shell scripts) - shell script platform?
|
25
|
+
* Easy on the eye and memory; easy to learn how to use and remember how to access a script.
|
26
|
+
* Out-of-the-box supported script sources: GitHub repos, Gist, Pastie, Pastebin, Raw/URL.
|
27
|
+
* Local script aliases for personalization - optional though.
|
28
|
+
|
29
|
+
h2. Installation
|
30
|
+
|
31
|
+
<pre>$ sudo gem install grimen-shelly</pre>
|
32
|
+
|
33
|
+
h2. Dependencies
|
34
|
+
|
35
|
+
<pre>$ sudo gem install activesupport</pre>
|
36
|
+
<pre>$ sudo gem install octopi</pre>
|
37
|
+
|
38
|
+
h2. Basic Usage
|
39
|
+
|
40
|
+
Run *GitHub* script source:
|
41
|
+
|
42
|
+
<pre>$ shelly run github:grimen/my_shell_scripts/install_geoip-city.sh</pre>
|
43
|
+
|
44
|
+
Run *Gist* script source:
|
45
|
+
|
46
|
+
<pre>$ shelly run gist:112090</pre>
|
47
|
+
|
48
|
+
Run *Pastie* script source:
|
49
|
+
|
50
|
+
<pre>$ shelly run pastie:478898</pre>
|
51
|
+
|
52
|
+
Run *Pastebin* script source:
|
53
|
+
|
54
|
+
<pre>$ shelly run pastebin:23233</pre>
|
55
|
+
|
56
|
+
Run *custom* script source:
|
57
|
+
|
58
|
+
<pre>$ shelly run http://somedomain.com/my_scripts/quack.sh</pre>
|
59
|
+
|
60
|
+
Example Output:
|
61
|
+
|
62
|
+
<pre>$ shelly run pastie:478898
|
63
|
+
=============================================================================
|
64
|
+
[shelly]: Script source type: PASTIE
|
65
|
+
[shelly]: Script source URL: http://pastie.org/478898.txt
|
66
|
+
[shelly]: Fetching script...DONE
|
67
|
+
[shelly]: Executing script...
|
68
|
+
============================================================== SCRIPT =======
|
69
|
+
Quack, quack!
|
70
|
+
Quack, quack!
|
71
|
+
=============================================================================
|
72
|
+
[shelly]: Cleaning up...DONE
|
73
|
+
[shelly]: END</pre>
|
74
|
+
|
75
|
+
h2. Advanced Usage
|
76
|
+
|
77
|
+
h3. Repositories
|
78
|
+
|
79
|
+
Add repository:
|
80
|
+
|
81
|
+
<pre>$ shelly add repo github:grimen/my_shell_scripts</pre>
|
82
|
+
|
83
|
+
Note: The files within this repo is now accessible. Example-file: @install_geoip-city.sh@
|
84
|
+
|
85
|
+
Run:
|
86
|
+
|
87
|
+
<pre>$ shelly run install_geoip-city.sh</pre>
|
88
|
+
|
89
|
+
h3. Aliases
|
90
|
+
|
91
|
+
Add alias:
|
92
|
+
|
93
|
+
<pre>$ shelly add alias quack:gist/112090</pre>
|
94
|
+
|
95
|
+
Run:
|
96
|
+
|
97
|
+
<pre>$ shelly run quack</pre>
|
98
|
+
|
99
|
+
h2. NOTE: Security Implications
|
100
|
+
|
101
|
+
There are obvious security implications with running shell scripts from remote locations, but the same applies to scripts downloaded manually - which is why Shelly got born in the first place. As long as you know what you doing, security should not be a big issue really. You'll need root access (sudo) to run a script no matter if it's required by the script or not.
|
102
|
+
|
103
|
+
h2. Bugs & Features
|
104
|
+
|
105
|
+
Shelly is in a sort of conceptual stage, so a lot of improvements can be made. If you got any suggestions, issues, or find any vicious bugs, just file an issue or notify me.
|
106
|
+
|
107
|
+
h2. License
|
108
|
+
|
109
|
+
Copyright (c) 2009 Jonas Grimfelt, released under the MIT-license.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
NAME = "shelly"
|
6
|
+
SUMMARY = %Q{Remotely stored shell scripts runned locally - shell scripts á la rubygems sort of.}
|
7
|
+
HOMEPAGE = "http://github.com/grimen/#{NAME}/tree/master"
|
8
|
+
AUTHOR = "Jonas Grimfelt"
|
9
|
+
EMAIL = "grimen@gmail.com"
|
10
|
+
SUPPORT_FILES = %w(README.textile TODO.textile)
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = NAME
|
16
|
+
gem.summary = SUMMARY
|
17
|
+
gem.description = SUMMARY
|
18
|
+
gem.homepage = HOMEPAGE
|
19
|
+
gem.author = AUTHOR
|
20
|
+
gem.email = EMAIL
|
21
|
+
|
22
|
+
gem.require_paths = %w{lib}
|
23
|
+
gem.files = %w(MIT-LICENSE Rakefile) + SUPPORT_FILES + Dir.glob(File.join('{bin,config,lib,test}', '**', '*'))
|
24
|
+
gem.executables = %w(shelly)
|
25
|
+
gem.extra_rdoc_files = SUPPORT_FILES
|
26
|
+
end
|
27
|
+
rescue LoadError
|
28
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo NAME install technicalpickles-jeweler -s http://gems.github.com"
|
29
|
+
end
|
30
|
+
|
31
|
+
desc %Q{Run unit tests for "#{NAME}".}
|
32
|
+
task :default => :test
|
33
|
+
|
34
|
+
desc %Q{Run unit tests for "#{NAME}".}
|
35
|
+
Rake::TestTask.new(:test) do |test|
|
36
|
+
test.libs << 'lib'
|
37
|
+
test.pattern = 'test/**/*_test.rb'
|
38
|
+
test.verbose = true
|
39
|
+
end
|
40
|
+
|
41
|
+
desc %Q{Generate documentation for "#{NAME}".}
|
42
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = NAME
|
45
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '--charset=UTF-8'
|
46
|
+
rdoc.rdoc_files.include(SUPPORT_FILES)
|
47
|
+
rdoc.rdoc_files.include(File.join('lib', '**', '*.rb'))
|
48
|
+
end
|
data/TODO.textile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
h1. TODO
|
2
|
+
|
3
|
+
h2. Next
|
4
|
+
|
5
|
+
* Feature: shelly help
|
6
|
+
* Enhance: Make the shell script execution output more *stable*, i.e. not being printed out in wrong order
|
7
|
+
* Enhance: Arguments handling; make use of existing lib for nicer args-parsing?
|
8
|
+
* Feature: Write boring tests
|
9
|
+
* Other: Comment the code where needed - for everyone's best
|
10
|
+
* ____________?
|
data/bin/shelly
ADDED
data/config/shelly.yml
ADDED
data/lib/shelly.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'shelly', '**', '*.rb'))].uniq.each do |file|
|
2
|
+
require file
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Shelly
|
9
|
+
|
10
|
+
VALID_SCRIPT_SOURCES = [:github, :gist, :pastie, :pastebin, :raw].freeze
|
11
|
+
VALID_COMMANDS = {
|
12
|
+
:run => [],
|
13
|
+
:create => [:config],
|
14
|
+
:add => [:repo, :alias],
|
15
|
+
:remove => [:repo, :alias],
|
16
|
+
:list => [:repos, :aliases],
|
17
|
+
:help => []
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def run(args)
|
21
|
+
(args ||= []).collect! { |arg| arg.strip }
|
22
|
+
|
23
|
+
unless Shelly.internet_connection?
|
24
|
+
puts "[shelly]: You are not connected to the Internet."
|
25
|
+
else
|
26
|
+
unless args.empty?
|
27
|
+
if Shelly.valid_command?(args[0], args[1])
|
28
|
+
case args[0].to_sym
|
29
|
+
when :run then Shelly.run!(args[1])
|
30
|
+
when :create then Shelly.create!(args[1])
|
31
|
+
when :add then Shelly.add!(args[1], args[2])
|
32
|
+
when :remove then Shelly.remove!(args[1], args[2])
|
33
|
+
when :list then Shelly.list(args[1])
|
34
|
+
when :help then Shelly.help(args[1])
|
35
|
+
end
|
36
|
+
else
|
37
|
+
puts "[shelly]: FAIL: '#{args[1]}' is not a valid command for '#{args[0]}'. Try 'help' for help."
|
38
|
+
end
|
39
|
+
else
|
40
|
+
puts "[shelly]: FAIL: No valid command."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def run!(what)
|
47
|
+
if what
|
48
|
+
begin
|
49
|
+
if Shelly::Store::Alias.include?(what)
|
50
|
+
# Alias, e.g. "shelly run hello_world"
|
51
|
+
what = Shelly::Store::Alias.value(what)
|
52
|
+
elsif Shelly::Store::Repo.include?(what)
|
53
|
+
# File from repo, e.g. "shelly run hello_world"
|
54
|
+
what = Shelly::Store::Repo.value(what)
|
55
|
+
end
|
56
|
+
|
57
|
+
if what.include?(':')
|
58
|
+
# Explicit script source, e.g. "shelly run pastie:1234"
|
59
|
+
whats = what.split(':')
|
60
|
+
script_source = whats.shift.to_sym
|
61
|
+
script_id = whats.join('/')
|
62
|
+
|
63
|
+
if valid_script_source?(script_source)
|
64
|
+
case script_source
|
65
|
+
when :github then Shelly::ScriptSource::Scm::Github.new(script_id).run!
|
66
|
+
when :gist then Shelly::ScriptSource::Plain::Gist.new(script_id).run!
|
67
|
+
when :pastie then Shelly::ScriptSource::Plain::Pastie.new(script_id).run!
|
68
|
+
when :pastebin then Shelly::ScriptSource::Plain::Pastebin.new(script_id).run!
|
69
|
+
when :raw then Shelly::ScriptSource::Plain::Raw.new(script_id).run!
|
70
|
+
end
|
71
|
+
else
|
72
|
+
Shelly::ScriptSource::Plain::Raw.new(script_id).run!
|
73
|
+
end
|
74
|
+
else
|
75
|
+
puts "[shelly]: FAIL: No valid script source."
|
76
|
+
end
|
77
|
+
rescue StandardError => e
|
78
|
+
puts "#{e}"
|
79
|
+
end
|
80
|
+
else
|
81
|
+
puts "[shelly]: Invalid args for RUN."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def create!(what)
|
86
|
+
if what
|
87
|
+
case what.to_sym
|
88
|
+
when :config then Shelly::Store::Base.create_root_config!
|
89
|
+
end
|
90
|
+
else
|
91
|
+
puts "[shelly]: Invalid args for CREATE."
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def add!(to, what)
|
96
|
+
if to
|
97
|
+
case to.to_sym
|
98
|
+
when :repo then Shelly::Store::Repo.add(what)
|
99
|
+
when :alias then Shelly::Store::Alias.add(what)
|
100
|
+
end
|
101
|
+
else
|
102
|
+
puts "[shelly]: Invalid args for ADD."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def remove!(from, what)
|
107
|
+
if from
|
108
|
+
case from.to_sym
|
109
|
+
when :repo then Shelly::Store::Repo.remove(what)
|
110
|
+
when :alias then Shelly::Store::Alias.remove(what)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
puts "[shelly]: Invalid args for REMOVE."
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def list(from)
|
118
|
+
case (from ||= :all).to_sym
|
119
|
+
when :repos then puts Shelly::Store::Repo.list
|
120
|
+
when :aliases then puts Shelly::Store::Alias.list
|
121
|
+
else puts Shelly::Store::Base.list
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# TODO: Implement help instructions
|
126
|
+
def help(from)
|
127
|
+
puts "[shelly]: No help available."
|
128
|
+
end
|
129
|
+
|
130
|
+
extend self
|
131
|
+
|
132
|
+
def internet_connection?
|
133
|
+
uri = URI('http://google.com')
|
134
|
+
begin
|
135
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
136
|
+
http.open_timeout = 5
|
137
|
+
http.start
|
138
|
+
http.finish
|
139
|
+
true
|
140
|
+
rescue
|
141
|
+
false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def valid_alias?(arg)
|
146
|
+
Shelly::Store::Alias.include?(arg)
|
147
|
+
end
|
148
|
+
|
149
|
+
def valid_repo?(arg)
|
150
|
+
Shelly::Store::Repo.include?(arg)
|
151
|
+
end
|
152
|
+
|
153
|
+
def valid_repo_file?(arg)
|
154
|
+
Shelly::Store::Repo.include_file?(arg)
|
155
|
+
end
|
156
|
+
|
157
|
+
def valid_script_source?(arg)
|
158
|
+
VALID_SCRIPT_SOURCES.include?(arg.to_sym)
|
159
|
+
end
|
160
|
+
|
161
|
+
def valid_command?(*args)
|
162
|
+
if args[1]
|
163
|
+
VALID_COMMANDS[args[0].to_sym].empty? || VALID_COMMANDS[args[0].to_sym].include?(args[1].to_sym)
|
164
|
+
else
|
165
|
+
VALID_COMMANDS.keys.include?(args[0].to_sym)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Shelly
|
2
|
+
module ScriptSource
|
3
|
+
class Base
|
4
|
+
|
5
|
+
DEFAULT_SCRIPT_DIRECTORY = '/tmp'.freeze # could not get this to work
|
6
|
+
DEFAULT_SCRIPT_EXTENSION = ''.freeze
|
7
|
+
|
8
|
+
attr_accessor :script_file_name, :script_file_path, :fetch_url
|
9
|
+
|
10
|
+
def initialize(script_identifier)
|
11
|
+
self.fetch_url = self.script_url(script_identifier)
|
12
|
+
self.script_file_name = self.script_name(script_identifier)
|
13
|
+
self.script_file_path = self.script_path(self.script_file_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run!
|
17
|
+
begin
|
18
|
+
puts "============================================================================="
|
19
|
+
puts "[shelly]: Script source type: #{self.script_source.upcase}"
|
20
|
+
puts "[shelly]: Script source URL: #{self.fetch_url}"
|
21
|
+
|
22
|
+
print "[shelly]: Fetching script..."
|
23
|
+
Shelly::ShellScript.store!(self.fetch_url, self.script_file_path, 'script')
|
24
|
+
print "DONE\n"
|
25
|
+
|
26
|
+
puts "[shelly]: Executing script..."
|
27
|
+
puts "============================================================== SCRIPT ======="
|
28
|
+
Shelly::ShellScript.execute!(self.script_file_path)
|
29
|
+
puts "============================================================================="
|
30
|
+
|
31
|
+
print "[shelly]: Cleaning up..."
|
32
|
+
Shelly::ShellScript.delete!(self.script_file_path)
|
33
|
+
print "DONE\n"
|
34
|
+
|
35
|
+
puts "[shelly]: END\n"
|
36
|
+
|
37
|
+
rescue StandardError => m
|
38
|
+
puts "[shelly]: Error: #{m}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def script_source
|
45
|
+
self.class.to_s.split('::').last.downcase
|
46
|
+
end
|
47
|
+
|
48
|
+
def script_name(id)
|
49
|
+
"#{self.script_source}_#{id}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def script_path(script_name)
|
53
|
+
File.expand_path(File.join(DEFAULT_SCRIPT_DIRECTORY, "#{self.script_file_name}#{DEFAULT_SCRIPT_EXTENSION}"))
|
54
|
+
end
|
55
|
+
|
56
|
+
def script_url
|
57
|
+
raise "[shelly]: ** Error: Should implement 'script_url'."
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'base'))
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module ScriptSource
|
5
|
+
module Plain
|
6
|
+
class Base < Shelly::ScriptSource::Base
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def is_url(url)
|
11
|
+
url =~ /#{SCRIPT_SOURCE_BASE_URL}/
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module ScriptSource
|
5
|
+
module Plain
|
6
|
+
class Gist < Shelly::ScriptSource::Plain::Base
|
7
|
+
|
8
|
+
SCRIPT_SOURCE_BASE_URL = 'gist.github.com'.freeze
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def script_url(gist_id)
|
13
|
+
"http://#{SCRIPT_SOURCE_BASE_URL}/#{gist_id}.txt"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module ScriptSource
|
5
|
+
module Plain
|
6
|
+
class Pastebin < Shelly::ScriptSource::Plain::Base
|
7
|
+
|
8
|
+
SCRIPT_SOURCE_BASE_URL = 'pastebin.com'.freeze
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def script_url(pastebin_id)
|
13
|
+
"http://#{SCRIPT_SOURCE_BASE_URL}/pastebin.php?dl=#{pastebin_id}"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module ScriptSource
|
5
|
+
module Plain
|
6
|
+
class Pastie < Shelly::ScriptSource::Plain::Base
|
7
|
+
|
8
|
+
SCRIPT_SOURCE_BASE_URL = 'pastie.org'.freeze
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def script_url(pastie_id)
|
13
|
+
"http://#{SCRIPT_SOURCE_BASE_URL}/#{pastie_id}.txt"
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module Shelly
|
6
|
+
module ScriptSource
|
7
|
+
module Plain
|
8
|
+
class Raw < Shelly::ScriptSource::Plain::Base
|
9
|
+
|
10
|
+
SCRIPT_SOURCE_BASE_URL = '.'.freeze
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def script_url(url)
|
15
|
+
URI.encode("#{url}")
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
gem 'octopi', '0.0.9'
|
6
|
+
require 'octopi'
|
7
|
+
|
8
|
+
module Shelly
|
9
|
+
module ScriptSource
|
10
|
+
module Scm
|
11
|
+
class Github < Shelly::ScriptSource::Scm::Base
|
12
|
+
|
13
|
+
include Octopi
|
14
|
+
|
15
|
+
SCRIPT_SOURCE_BASE_URL = 'http://github.com'.freeze
|
16
|
+
|
17
|
+
def initialize(script_identifier)
|
18
|
+
user_id, repo_name, file_name, commit_sha = id_parts = script_identifier.split('/')
|
19
|
+
raise "Invalid script source: '#{script_identifier}'." unless id_parts.size >= 2
|
20
|
+
|
21
|
+
self.fetch_url = self.script_url(user_id, repo_name, file_name, commit_sha)
|
22
|
+
self.script_file_name = self.script_name(file_name)
|
23
|
+
self.script_file_path = self.script_path(self.script_file_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.files(user_id, repo_name, commit_id = nil)
|
27
|
+
commit_id ||= Repository.find(user_id, repo_name).commits.first.id
|
28
|
+
repo_files = FileObject.find(user_id, repo_name, commit_id).collect { |f| f.name }
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def script_url(user_id, repo_name, file_name, commit_id = nil)
|
34
|
+
commit_id ||= Repository.find(user_id, repo_name).commits.first.id
|
35
|
+
"#{SCRIPT_SOURCE_BASE_URL}/#{user_id}/#{repo_name}/raw/#{commit_id}/#{file_name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Shelly
|
2
|
+
module ShellScript
|
3
|
+
|
4
|
+
def store!(source_url, destination_path, type)
|
5
|
+
`sudo curl "#{source_url}" --silent -o #{destination_path}`
|
6
|
+
raise "The specified #{type} could not be found." unless File.exist?(destination_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def make_executable!(script_path)
|
10
|
+
`sudo chmod 777 #{script_path}` if File.exist?(script_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute!(script_path)
|
14
|
+
if self.valid?(script_path)
|
15
|
+
self.make_executable!(script_path)
|
16
|
+
self.ensure_format!(script_path)
|
17
|
+
self.execute_with_output!(script_path, false)
|
18
|
+
else
|
19
|
+
puts "Invalid script source."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def execute_with_output!(script_path, advanced = false)
|
24
|
+
if advanced
|
25
|
+
# Runs the script with output directly, but causes paths to be wrong. =/
|
26
|
+
File.open(script_path, 'r') do |file|
|
27
|
+
begin
|
28
|
+
while line = file.gets
|
29
|
+
# Print shell script instruction
|
30
|
+
# print line
|
31
|
+
# Run everyting except empty lines and comments + print out result.
|
32
|
+
print `#{line.strip}` unless line =~ /^(\#|\n)/
|
33
|
+
end
|
34
|
+
rescue StandardError => e
|
35
|
+
puts "Error: #{e}"
|
36
|
+
ensure
|
37
|
+
file.close
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
puts `sudo #{script_path}`
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete!(script_path)
|
46
|
+
`sudo rm #{script_path}` if File.exist?(script_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?(script_path)
|
50
|
+
result = false
|
51
|
+
File.open(script_path, 'r') do |file|
|
52
|
+
begin
|
53
|
+
script = file.read
|
54
|
+
# Contains a shebang? I.e. is it a valid script?
|
55
|
+
result = (script =~ /^\#\!\//)
|
56
|
+
rescue StandardError => e
|
57
|
+
puts "Error: #{e}"
|
58
|
+
ensure
|
59
|
+
file.close
|
60
|
+
end
|
61
|
+
end
|
62
|
+
result
|
63
|
+
end
|
64
|
+
|
65
|
+
def ensure_format!(script_path)
|
66
|
+
File.open(script_path, 'r+') do |file|
|
67
|
+
begin
|
68
|
+
script = file.read
|
69
|
+
# Remove invalid new-line returns
|
70
|
+
script.gsub!(/\r/, '')
|
71
|
+
file.write(script)
|
72
|
+
rescue StandardError => e
|
73
|
+
puts "Error: #{e}"
|
74
|
+
ensure
|
75
|
+
file.close
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
extend self
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module Store
|
5
|
+
class Alias < Shelly::Store::Base
|
6
|
+
|
7
|
+
CONFIG_KEY = 'aliases'.freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def add(what)
|
12
|
+
whats = what.split(':')
|
13
|
+
config = self.load_config
|
14
|
+
config[CONFIG_KEY] ||= {}
|
15
|
+
config[CONFIG_KEY].merge!(whats.shift => whats.join(':'))
|
16
|
+
self.store_config(config)
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove(what)
|
20
|
+
whats = what.split(':')
|
21
|
+
config = self.load_config
|
22
|
+
config[CONFIG_KEY].delete(whats.shift)
|
23
|
+
config[CONFIG_KEY] = nil if config[CONFIG_KEY] == {}
|
24
|
+
self.store_config(config)
|
25
|
+
end
|
26
|
+
|
27
|
+
def list
|
28
|
+
self.config.to_yaml
|
29
|
+
end
|
30
|
+
|
31
|
+
def include?(key)
|
32
|
+
config = load_config
|
33
|
+
config[CONFIG_KEY].keys.include?(key) rescue false
|
34
|
+
end
|
35
|
+
|
36
|
+
def value(key)
|
37
|
+
"#{config[CONFIG_KEY][key.to_s]}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Shelly
|
4
|
+
module Store
|
5
|
+
class Base
|
6
|
+
|
7
|
+
DEFAULT_CONFIG_FILE_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'shelly.yml')).freeze
|
8
|
+
CUSTOM_CONFIG_FILE_PATH = File.expand_path(File.join('~', '.shelly')).freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def config
|
13
|
+
load_config
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_root_config!
|
17
|
+
`sudo cp #{self::DEFAULT_CONFIG_FILE_PATH} #{self::CUSTOM_CONFIG_FILE_PATH}`
|
18
|
+
`sudo chmod 664 #{self::CUSTOM_CONFIG_FILE_PATH}`
|
19
|
+
end
|
20
|
+
|
21
|
+
def config_file_path
|
22
|
+
File.exist?(self::CUSTOM_CONFIG_FILE_PATH) ? self::CUSTOM_CONFIG_FILE_PATH : self::DEFAULT_CONFIG_FILE_PATH
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_config
|
26
|
+
config_hash = YAML.load_file(self.config_file_path) || Hash.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def store_config(config_hash)
|
30
|
+
File.open(config_file_path, 'w') do |file|
|
31
|
+
YAML.dump(config_hash, file)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def list
|
36
|
+
load_config.to_yaml rescue ''
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'base'))
|
2
|
+
|
3
|
+
require 'activesupport'
|
4
|
+
|
5
|
+
module Shelly
|
6
|
+
module Store
|
7
|
+
class Repo < Shelly::Store::Base
|
8
|
+
|
9
|
+
CONFIG_KEY = 'repos'.freeze
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def add(what)
|
14
|
+
whats = what.split(':')
|
15
|
+
config = self.load_config
|
16
|
+
config[CONFIG_KEY] ||= {}
|
17
|
+
config[CONFIG_KEY][whats[0].to_s] ||= []
|
18
|
+
config[CONFIG_KEY][whats[0].to_s] << whats[1]
|
19
|
+
self.store_config(config)
|
20
|
+
end
|
21
|
+
|
22
|
+
def remove(what)
|
23
|
+
whats = what.split(':')
|
24
|
+
config = self.load_config
|
25
|
+
config[CONFIG_KEY][whats[0].to_s].delete(whats[1].to_s) if config[CONFIG_KEY][whats[0].to_s]
|
26
|
+
config[CONFIG_KEY][whats[0].to_s] = nil if config[CONFIG_KEY][whats[0].to_s] == []
|
27
|
+
self.store_config(config)
|
28
|
+
end
|
29
|
+
|
30
|
+
def list
|
31
|
+
self.load_config[CONFIG_KEY].to_yaml
|
32
|
+
end
|
33
|
+
|
34
|
+
def include?(file_name)
|
35
|
+
files(file_name).present? rescue false
|
36
|
+
end
|
37
|
+
|
38
|
+
def files(match_file_name = nil)
|
39
|
+
@repo = nil
|
40
|
+
@file_names = []
|
41
|
+
config = load_config
|
42
|
+
config[CONFIG_KEY].each do |host, repos|
|
43
|
+
scm_class = "Shelly::ScriptSource::Scm::#{host.classify}".constantize
|
44
|
+
repos.each do |repo|
|
45
|
+
files = scm_class.files(*repo.split('/'))
|
46
|
+
@file_names << files
|
47
|
+
@repo = "#{host}:#{repo}/#{match_file_name}" if files.include?(match_file_name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
@file_names = @file_names.flatten.compact
|
51
|
+
@repo
|
52
|
+
end
|
53
|
+
|
54
|
+
def value(file_name)
|
55
|
+
files(file_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/test/shelly_test.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
|
5
|
+
require 'shelly'
|
6
|
+
|
7
|
+
class ShellyTest < Test::Unit::TestCase
|
8
|
+
|
9
|
+
context "script source" do
|
10
|
+
should "should be able to fetch: gist" do
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
should "should be able to fetch: pastebin" do
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
should "should be able to fetch: pastie" do
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
should "should be able to fetch: raw" do
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "store" do
|
28
|
+
# should "create config file in user root upon request" do
|
29
|
+
# `rm #{Shelly::Store::Base::CUSTOM_CONFIG_FILE_PATH}`
|
30
|
+
# Shelly.create!(:config)
|
31
|
+
#
|
32
|
+
# assert File.exist?(Shelly::Store::Base::CUSTOM_CONFIG_FILE_PATH)
|
33
|
+
# end
|
34
|
+
|
35
|
+
should "initialize config" do
|
36
|
+
assert Shelly::Store::Base.load_config
|
37
|
+
end
|
38
|
+
|
39
|
+
should "be able to add alias to config" do
|
40
|
+
Shelly.add(:alias, 'foo:http://pastie.org/12345')
|
41
|
+
|
42
|
+
assert_equal Shelly::Store::Base.config['aliases']['foo'], 'http://pastie.org/12345'
|
43
|
+
end
|
44
|
+
|
45
|
+
should "be able to remove alias from config" do
|
46
|
+
Shelly.add(:alias, 'foo:http://pastie.org/12345')
|
47
|
+
Shelly.remove(:alias, 'foo')
|
48
|
+
|
49
|
+
assert !Shelly::Store::Base.config['aliases'].keys.include?('foo')
|
50
|
+
end
|
51
|
+
|
52
|
+
should "be able to add repo to config" do
|
53
|
+
Shelly.add(:repo, 'github/grimen/shelly')
|
54
|
+
|
55
|
+
#flunk 'Not done testing yet.'
|
56
|
+
|
57
|
+
#assert false
|
58
|
+
end
|
59
|
+
|
60
|
+
should "be able to remove repo from config" do
|
61
|
+
Shelly.remove(:repo, 'github/grimen/shelly')
|
62
|
+
|
63
|
+
#assert false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grimen-shelly
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonas Grimfelt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-19 00:00:00 -07:00
|
13
|
+
default_executable: shelly
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "Remotely stored shell scripts runned locally - shell scripts \xC3\xA1 la rubygems sort of."
|
17
|
+
email: grimen@gmail.com
|
18
|
+
executables:
|
19
|
+
- shelly
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.textile
|
24
|
+
- TODO.textile
|
25
|
+
files:
|
26
|
+
- MIT-LICENSE
|
27
|
+
- README.textile
|
28
|
+
- Rakefile
|
29
|
+
- TODO.textile
|
30
|
+
- bin/shelly
|
31
|
+
- config/shelly.yml
|
32
|
+
- lib/shelly.rb
|
33
|
+
- lib/shelly/script_source/base.rb
|
34
|
+
- lib/shelly/script_source/plain/base.rb
|
35
|
+
- lib/shelly/script_source/plain/gist.rb
|
36
|
+
- lib/shelly/script_source/plain/pastebin.rb
|
37
|
+
- lib/shelly/script_source/plain/pastie.rb
|
38
|
+
- lib/shelly/script_source/plain/raw.rb
|
39
|
+
- lib/shelly/script_source/scm/base.rb
|
40
|
+
- lib/shelly/script_source/scm/github.rb
|
41
|
+
- lib/shelly/shell_script.rb
|
42
|
+
- lib/shelly/store/alias.rb
|
43
|
+
- lib/shelly/store/base.rb
|
44
|
+
- lib/shelly/store/repo.rb
|
45
|
+
- test/shelly_test.rb
|
46
|
+
has_rdoc: false
|
47
|
+
homepage: http://github.com/grimen/shelly/tree/master
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --charset=UTF-8
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.2.0
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: "Remotely stored shell scripts runned locally - shell scripts \xC3\xA1 la rubygems sort of."
|
72
|
+
test_files:
|
73
|
+
- test/shelly_test.rb
|