jeni 0.2.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.
- 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
|