jeni 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Bugs.rdoc +6 -0
- data/Gemfile +14 -0
- data/History.txt +9 -0
- data/Intro.txt +3 -0
- data/LICENCE.rdoc +159 -0
- data/README.md +188 -0
- data/lib/jeni.rb +374 -0
- data/lib/jeni/actions.rb +400 -0
- data/lib/jeni/errors.rb +22 -0
- data/lib/jeni/io.rb +71 -0
- data/lib/jeni/options.rb +68 -0
- data/lib/jeni/optparse.rb +84 -0
- data/lib/jeni/utils.rb +181 -0
- data/lib/jeni/version.rb +13 -0
- data/spec/jeni_spec.rb +85 -0
- data/spec/jeni_utils_spec.rb +297 -0
- data/spec/spec_helper.rb +26 -0
- data/test/examples/source/coati.haml.conf +37 -0
- data/test/examples/source/executable +3 -0
- data/test/examples/source/jenny-diff.rb +64 -0
- data/test/examples/source/jenny.rb +63 -0
- data/test/examples/source/shebang.rb +3 -0
- data/test/examples/source/subfiles/subfile_1.rb +63 -0
- data/test/examples/source/template.haml.rb +10 -0
- data/test/examples/target/archive/coati.haml.conf +37 -0
- data/test/examples/target/archive/executable +3 -0
- data/test/examples/target/archive/jenny-diff.rb +64 -0
- data/test/examples/target/archive/jenny.rb +63 -0
- data/test/examples/target/archive/shebang.rb +3 -0
- data/test/examples/target/archive/subfiles/subfile_1.rb +63 -0
- data/test/examples/target/archive/template.haml.rb +10 -0
- data/test/examples/target/jenny.rb +63 -0
- data/test/examples/target/jenny_link.rb +63 -0
- data/test/examples/target2/coati.conf +36 -0
- data/test/examples/target2/coati.haml.conf +37 -0
- data/test/examples/target2/executable +3 -0
- data/test/examples/target2/jenny-diff.rb +64 -0
- data/test/examples/target2/jenny.rb +63 -0
- data/test/examples/target2/jenny_link.rb +63 -0
- data/test/examples/target2/jenny_template.rb +10 -0
- data/test/examples/target2/jenny_test.rb +63 -0
- data/test/examples/target2/shebang.rb +3 -0
- data/test/examples/target2/std_template.rb +12 -0
- data/test/examples/target2/template.haml.rb +10 -0
- data/test/examples/test1.rb +30 -0
- data/test/examples/test2.rb +27 -0
- data/test/examples/test_args +24 -0
- data/test/examples/test_users +16 -0
- metadata +162 -0
data/lib/jeni/actions.rb
ADDED
@@ -0,0 +1,400 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Title
|
4
|
+
#
|
5
|
+
# == SubTitle
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2012 Robert Sharp
|
9
|
+
# License:: Open Software Licence v3.0
|
10
|
+
#
|
11
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
12
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
13
|
+
# and in the file copyright.txt. Under the terms of this licence, all derivative works
|
14
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
15
|
+
#
|
16
|
+
#
|
17
|
+
#
|
18
|
+
|
19
|
+
module Jeni
|
20
|
+
|
21
|
+
# Implementation mixin containing methods for each action that Jeni carries out
|
22
|
+
module Actions
|
23
|
+
|
24
|
+
# Create a copy of the source file as the target file
|
25
|
+
#
|
26
|
+
# the checks are really redundant, but have been left in as belt and braces
|
27
|
+
#
|
28
|
+
def copy(source, target)
|
29
|
+
# check the source exists
|
30
|
+
puts "Copying #{source} to #{target}" if @verbose
|
31
|
+
unless FileTest.exists?(source)
|
32
|
+
say(:missing, source, :error)
|
33
|
+
return nil
|
34
|
+
end
|
35
|
+
#check the target directory is writable
|
36
|
+
target_dir = File.dirname(target)
|
37
|
+
unless FileTest.writable?(target_dir)
|
38
|
+
say(:unwritable, target_dir, :error)
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
# check if the target exists
|
42
|
+
if FileTest.exists?(target) then
|
43
|
+
|
44
|
+
unless FileUtils.compare_file(source, target) then
|
45
|
+
say(:exists, target, :warning)
|
46
|
+
loop do
|
47
|
+
case ask('Do you want to overwrite', :no)
|
48
|
+
when :yes
|
49
|
+
say(:overwrite, target, :ok)
|
50
|
+
break
|
51
|
+
when :no, :skip
|
52
|
+
say(:skipping, target, :warning)
|
53
|
+
return nil
|
54
|
+
when :diff
|
55
|
+
puts Diffy::Diff.new(target, source, :source=>'files').to_s(:color)
|
56
|
+
when :list
|
57
|
+
File.open(target) do |tfile|
|
58
|
+
tfile.readline do |line|
|
59
|
+
puts line
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end # case
|
63
|
+
end # loop
|
64
|
+
else # unless
|
65
|
+
# files are the same
|
66
|
+
say(:identical, target, :no_change)
|
67
|
+
return target
|
68
|
+
end # unless
|
69
|
+
else # if
|
70
|
+
say(:create, target, :ok)
|
71
|
+
end
|
72
|
+
|
73
|
+
FileUtils.cp(source, target) unless @pretend
|
74
|
+
return target
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# make the given directory, and any intermediates
|
79
|
+
def mkdir(target)
|
80
|
+
if FileTest.directory?(target)
|
81
|
+
say(:identical, target, :no_change)
|
82
|
+
elsif @pretend then
|
83
|
+
say(:mkdir, target, :ok)
|
84
|
+
else
|
85
|
+
FileUtils.mkdir_p(target)
|
86
|
+
if FileTest.directory?(target) then
|
87
|
+
say(:mkdir, target, :ok)
|
88
|
+
else
|
89
|
+
say(:mkdir, "Failed to make #{target}", :error)
|
90
|
+
continue?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# change the owner of a file
|
96
|
+
def chown(file, owner)
|
97
|
+
message = "#{file} to #{owner}"
|
98
|
+
if @pretend then
|
99
|
+
say(:chown, message, :ok)
|
100
|
+
return
|
101
|
+
end
|
102
|
+
if FileTest.exists?(file) then
|
103
|
+
FileUtils.chown(owner, nil, file)
|
104
|
+
say(:chown, message, :ok)
|
105
|
+
else
|
106
|
+
say(:missing, file, :error)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# change the group of a file
|
111
|
+
def chgrp(file, owner)
|
112
|
+
message = "#{file} to #{owner}"
|
113
|
+
if @pretend then
|
114
|
+
say(:chgrp, message, :ok)
|
115
|
+
return
|
116
|
+
end
|
117
|
+
if FileTest.exists?(file) then
|
118
|
+
FileUtils.chown(nil, owner, file)
|
119
|
+
say(:chgrp, message, :ok)
|
120
|
+
else
|
121
|
+
say(:missing, file, :error)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# make a file executable
|
126
|
+
def chmod(file, mode)
|
127
|
+
message = "#{file} to #{mode.to_s(8)}"
|
128
|
+
if @pretend then
|
129
|
+
say(:chmod, message, :ok)
|
130
|
+
elsif FileTest.exists?(file) then
|
131
|
+
FileUtils.chmod(mode,file)
|
132
|
+
say(:chmod, message, :ok)
|
133
|
+
else
|
134
|
+
say(:missing, file, :error)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# generate a file from a template, which is done even if the user
|
139
|
+
# request pretend. This ensures that a proper comparison is made
|
140
|
+
# with any existing file
|
141
|
+
def generate(template_file, target_file, locals={})
|
142
|
+
template = [":plain\n"]
|
143
|
+
# read in the file, prepending 2 spaces to each line
|
144
|
+
File.open(template_file) do |tfile|
|
145
|
+
tfile.each_line do |tline|
|
146
|
+
template << " " + tline
|
147
|
+
end
|
148
|
+
end
|
149
|
+
# now create the template as an engine and render it
|
150
|
+
engine = Haml::Engine.new(template.join)
|
151
|
+
target = engine.render(binding, locals)
|
152
|
+
|
153
|
+
puts target if @verbose
|
154
|
+
|
155
|
+
# and write it out to the target, but make it a temp
|
156
|
+
temp_file = target_file + "._temp"
|
157
|
+
File.open(temp_file, 'w') do |tfile|
|
158
|
+
target.each_line do |tline|
|
159
|
+
tfile.puts tline
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# and now copy the temp file to the target
|
164
|
+
copy(temp_file, target_file)
|
165
|
+
|
166
|
+
# and finally clean up
|
167
|
+
FileUtils.rm_f(temp_file)
|
168
|
+
end
|
169
|
+
|
170
|
+
# return the path to the standard template, which can be found in
|
171
|
+
# the places search below.
|
172
|
+
def get_template(source)
|
173
|
+
gsource = File.expand_path(File.join('.jermine', 'templates', source), '~')
|
174
|
+
unless File.exists?(gsource)
|
175
|
+
gsource = File.join('usr', 'local', 'share', 'templates', source)
|
176
|
+
unless File.exists(gsource)
|
177
|
+
@errors[:missing] = 'Standard template file: #{source}'
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
return gsource
|
182
|
+
end
|
183
|
+
|
184
|
+
# create a wrapper for a file, which is specific to a gem
|
185
|
+
def wrap(source, target, fullsource)
|
186
|
+
#create the wrapper somewhere
|
187
|
+
basename = File.basename(source)
|
188
|
+
tempname = File.join('/tmp', File.basename(source) + ".wrapper")
|
189
|
+
File.open(tempname, 'w') {|f| f.write wrap_file(source, fullsource)}
|
190
|
+
copy(tempname, target)
|
191
|
+
end
|
192
|
+
|
193
|
+
# create a link to a file
|
194
|
+
def link_it(source, target)
|
195
|
+
if FileTest.exists?(source) then
|
196
|
+
if FileTest.symlink?(target) then
|
197
|
+
# already got a link, is it identical
|
198
|
+
existing_link = File.readlink(target)
|
199
|
+
if existing_link == source then
|
200
|
+
say(:identical, target, :no_change)
|
201
|
+
else
|
202
|
+
say(:exists, "#{target} -> #{existing_link}", :warning)
|
203
|
+
loop do
|
204
|
+
case ask('Do you want to overwrite', :no, 'ynsl')
|
205
|
+
when :yes
|
206
|
+
say(:overwrite, target, :ok)
|
207
|
+
break
|
208
|
+
when :no, :skip
|
209
|
+
say(:skipping, target, :warning)
|
210
|
+
return nil
|
211
|
+
else
|
212
|
+
puts "Link name: #{target}"
|
213
|
+
puts "New target: #{target}"
|
214
|
+
puts "Existing target: #{existing_link}"
|
215
|
+
end # case
|
216
|
+
end # loop
|
217
|
+
FileUtils.ln_sf(source, target) unless @pretend
|
218
|
+
say(:link, target, :ok)
|
219
|
+
end # if
|
220
|
+
elsif FileTest.exists?(target)
|
221
|
+
say(:exists, "as file: #{target}", :warning)
|
222
|
+
loop do
|
223
|
+
case ask('Do you want to replace it', :no, 'yns')
|
224
|
+
when :yes
|
225
|
+
say(:replace, target, :ok)
|
226
|
+
break
|
227
|
+
when :no, :skip
|
228
|
+
say(:skipping, target, :warning)
|
229
|
+
return nil
|
230
|
+
else
|
231
|
+
puts "Please make a valid selection"
|
232
|
+
end # case
|
233
|
+
end # loop
|
234
|
+
FileUtils.rm_f(target) unless @pretend
|
235
|
+
FileUtils.ln_s(source, target) unless @pretend
|
236
|
+
say(:link, target, :ok)
|
237
|
+
else
|
238
|
+
FileUtils.ln_s(source, target) unless @pretend
|
239
|
+
say(:link, target, :ok)
|
240
|
+
end
|
241
|
+
else
|
242
|
+
say(:missing, source, :error)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
# create a wrapper script as text
|
248
|
+
def wrap_file(source, fullsource)
|
249
|
+
|
250
|
+
return <<-TEXT
|
251
|
+
#{shebang fullsource}
|
252
|
+
#
|
253
|
+
# This file was generated by RubyGems.
|
254
|
+
#
|
255
|
+
# The application '#{@app_name}' is installed as part of a gem, and
|
256
|
+
# this file is here to facilitate running it.
|
257
|
+
#
|
258
|
+
# Updated to wrap non-bin executables
|
259
|
+
#
|
260
|
+
|
261
|
+
require 'rubygems'
|
262
|
+
|
263
|
+
version = "#{Gem::Requirement.default}"
|
264
|
+
|
265
|
+
# check if there is a version spec here, e.g.: gem_name _0.1.2_ args
|
266
|
+
if ARGV.first
|
267
|
+
str = ARGV.first
|
268
|
+
str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
|
269
|
+
if str =~ /\\A_(.*)_\\z/
|
270
|
+
# there is, so get it
|
271
|
+
version = $1
|
272
|
+
ARGV.shift
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
gem_spec = Gem::Specification.find_by_name('#{@app_name}', version)
|
277
|
+
gem_path = File.join(gem_spec.gem_dir, '#{source}')
|
278
|
+
|
279
|
+
gem '#{@app_name}', version
|
280
|
+
load gem_path
|
281
|
+
TEXT
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Generates a #! line for +bin_file_name+'s wrapper copying arguments if
|
287
|
+
# necessary.
|
288
|
+
#
|
289
|
+
# If the :custom_shebang config is set, then it is used as a template
|
290
|
+
# for how to create the shebang used for to run a gem's executables.
|
291
|
+
#
|
292
|
+
# The template supports 4 expansions:
|
293
|
+
#
|
294
|
+
# $env the path to the unix env utility
|
295
|
+
# $ruby the path to the currently running ruby interpreter
|
296
|
+
# $exec the path to the gem's executable
|
297
|
+
# $name the name of the gem the executable is for
|
298
|
+
#
|
299
|
+
def shebang(source)
|
300
|
+
|
301
|
+
env_paths = %w[/usr/bin/env /bin/env]
|
302
|
+
|
303
|
+
ruby_name = Gem::ConfigMap[:ruby_install_name] #if @env_shebang
|
304
|
+
|
305
|
+
first_line = File.open(source, "rb") {|file| file.gets}
|
306
|
+
env_path = nil
|
307
|
+
|
308
|
+
if /\A#!/ =~ first_line then
|
309
|
+
# Preserve extra words on shebang line, like "-w". Thanks RPA.
|
310
|
+
shebang = first_line.sub(/\A\#!.*?ruby\S*((\s+\S+)+)/, "#!#{Gem.ruby}")
|
311
|
+
opts = $1
|
312
|
+
shebang.strip! # Avoid nasty ^M issues.
|
313
|
+
end
|
314
|
+
|
315
|
+
if which = Gem.configuration[:custom_shebang]
|
316
|
+
which = which.gsub(/\$(\w+)/) do
|
317
|
+
case $1
|
318
|
+
when "env"
|
319
|
+
env_path ||= env_paths.find do |e_path|
|
320
|
+
File.executable? e_path
|
321
|
+
end
|
322
|
+
when "ruby"
|
323
|
+
"#{Gem.ruby}#{opts}"
|
324
|
+
when "exec"
|
325
|
+
bin_file_name
|
326
|
+
when "name"
|
327
|
+
spec.name
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
return "#!#{which}"
|
332
|
+
end
|
333
|
+
|
334
|
+
if not ruby_name then
|
335
|
+
"#!#{Gem.ruby}#{opts}"
|
336
|
+
elsif opts then
|
337
|
+
"#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}"
|
338
|
+
else
|
339
|
+
# Create a plain shebang line.
|
340
|
+
env_path ||= env_paths.find {|e_path| File.executable? e_path }
|
341
|
+
"#!#{env_path} #{ruby_name}"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# create a new user
|
346
|
+
def add_user(username, opts={})
|
347
|
+
if opts[:skip] then
|
348
|
+
say(:user, "#{username} already exists", :warning)
|
349
|
+
return false
|
350
|
+
end
|
351
|
+
|
352
|
+
home = opts[:home] || "/home/#{username}"
|
353
|
+
shell = opts[:shell] || "/bin/bash"
|
354
|
+
uid = opts[:uid]
|
355
|
+
gid = opts[:gid]
|
356
|
+
user_group = opts[:user_group]
|
357
|
+
cmd = "/usr/sbin/useradd -d #{home} -s #{shell}"
|
358
|
+
|
359
|
+
cmd << " -u #{uid}" if uid
|
360
|
+
cmd << " -g #{gid}" if gid
|
361
|
+
cmd << " -U" if user_group
|
362
|
+
cmd << " #{username}"
|
363
|
+
|
364
|
+
unless @pretend
|
365
|
+
system(cmd)
|
366
|
+
raise JeniError if $? != 0
|
367
|
+
end
|
368
|
+
say(:user, "Added user #{username}", :ok)
|
369
|
+
|
370
|
+
rescue JeniError
|
371
|
+
say(:user, "Failed to add user #{username} with error #{$?}", :error)
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
# create a new group
|
376
|
+
def add_group(group, opts={})
|
377
|
+
if opts[:skip] then
|
378
|
+
say(:group, "#{group} already exists", :warning)
|
379
|
+
return false
|
380
|
+
end
|
381
|
+
|
382
|
+
gid = opts[:gid]
|
383
|
+
cmd = "/usr/sbin/groupadd "
|
384
|
+
|
385
|
+
cmd << "-g #{gid}" if gid
|
386
|
+
cmd << " #{group}"
|
387
|
+
|
388
|
+
unless @pretend
|
389
|
+
system(cmd)
|
390
|
+
raise JeniError if $? != 0
|
391
|
+
end
|
392
|
+
say(:group, "Added group #{group}", :ok)
|
393
|
+
|
394
|
+
rescue JeniError
|
395
|
+
say(:group, "Failed to add user #{username} with error #{$?}", :error)
|
396
|
+
|
397
|
+
end
|
398
|
+
|
399
|
+
end
|
400
|
+
end
|
data/lib/jeni/errors.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#
|
2
|
+
# Author:: R.J.Sharp
|
3
|
+
# Email:: robert(a)osburn-sharp.ath.cx
|
4
|
+
# Copyright:: Copyright (c) 2012
|
5
|
+
# License:: Open Software Licence v3.0
|
6
|
+
#
|
7
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
8
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
9
|
+
# and in the file LICENCE. Under the terms of this licence, all derivative works
|
10
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# This file groups together all the errors for jeni.
|
14
|
+
# Preceed each class with a description of the error
|
15
|
+
|
16
|
+
module Jeni
|
17
|
+
|
18
|
+
# A general class for all errors created by this project. All specific exceptions
|
19
|
+
# should be children of this class
|
20
|
+
class JeniError < RuntimeError; end
|
21
|
+
|
22
|
+
end
|
data/lib/jeni/io.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
# = Jeni
|
4
|
+
#
|
5
|
+
# == say method
|
6
|
+
#
|
7
|
+
# Author:: Robert Sharp
|
8
|
+
# Copyright:: Copyright (c) 2012 Robert Sharp
|
9
|
+
# License:: Open Software Licence v3.0
|
10
|
+
#
|
11
|
+
# This software is licensed for use under the Open Software Licence v. 3.0
|
12
|
+
# The terms of this licence can be found at http://www.opensource.org/licenses/osl-3.0.php
|
13
|
+
# and in the file copyright.txt. Under the terms of this licence, all derivative works
|
14
|
+
# must themselves be licensed under the Open Software Licence v. 3.0
|
15
|
+
#
|
16
|
+
#
|
17
|
+
#
|
18
|
+
|
19
|
+
|
20
|
+
module Jeni
|
21
|
+
|
22
|
+
# Implementation mixin for IO related methods
|
23
|
+
module IO
|
24
|
+
|
25
|
+
# lookup that maps status symbol onto colours
|
26
|
+
Colours = Hash.new(:white).merge({:ok=>:green,
|
27
|
+
:no_change=>:blue,
|
28
|
+
:warning=>:yellow,
|
29
|
+
:error=>:red
|
30
|
+
})
|
31
|
+
|
32
|
+
# lookup to map between symbols and responses
|
33
|
+
Answers = Hash.new(false).merge({:yes=>'y', :no=>'n', :skip=>'s', :diff=>'d', :list=>'l'})
|
34
|
+
AnswerKeys = Answers.values.join('')
|
35
|
+
|
36
|
+
# set the length of the field in which verbs are set, right-justified
|
37
|
+
VerbLen = 15
|
38
|
+
|
39
|
+
# print out a message with nice indenting and colours
|
40
|
+
def say(verb, message, status=:ok, quiet=false)
|
41
|
+
mstr = verb.to_s.rjust(VerbLen) + ": "
|
42
|
+
mstr << message
|
43
|
+
mstr = mstr.send(Colours[status])
|
44
|
+
puts mstr unless @quiet || quiet
|
45
|
+
return mstr
|
46
|
+
end
|
47
|
+
|
48
|
+
# ask a question and give back the answer as a symbol using
|
49
|
+
# the Answers constant hash to get the symbol
|
50
|
+
def ask(question, default=:no, options=AnswerKeys)
|
51
|
+
default = :no unless Answers.has_key?(default)
|
52
|
+
return default if @pretend && ! @answer
|
53
|
+
def_key = Answers[default]
|
54
|
+
answers = (options.split(//) & AnswerKeys.split(//)).collect {|k| k == def_key ? k.upcase : k}.join('')
|
55
|
+
print "#{question}(#{answers})? "
|
56
|
+
response = gets.chomp.downcase
|
57
|
+
if Answers.has_value?(response) then
|
58
|
+
return Answers.index(response)
|
59
|
+
else
|
60
|
+
return default
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# check if the user wants to continue
|
65
|
+
def continue?
|
66
|
+
ans = ask("Do you want to continue?")
|
67
|
+
raise JeniError unless ans == :yes
|
68
|
+
end
|
69
|
+
|
70
|
+
end #IO
|
71
|
+
end
|