treequel 1.0.1 → 1.0.4
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/ChangeLog +176 -14
- data/LICENSE +1 -1
- data/Rakefile +61 -45
- data/Rakefile.local +20 -0
- data/bin/treequel +502 -269
- data/examples/ldap-rack-auth.rb +2 -0
- data/lib/treequel.rb +221 -18
- data/lib/treequel/branch.rb +410 -201
- data/lib/treequel/branchcollection.rb +25 -13
- data/lib/treequel/branchset.rb +42 -40
- data/lib/treequel/constants.rb +233 -3
- data/lib/treequel/control.rb +95 -0
- data/lib/treequel/controls/contentsync.rb +138 -0
- data/lib/treequel/controls/pagedresults.rb +162 -0
- data/lib/treequel/controls/sortedresults.rb +216 -0
- data/lib/treequel/directory.rb +212 -65
- data/lib/treequel/exceptions.rb +11 -12
- data/lib/treequel/filter.rb +1 -12
- data/lib/treequel/mixins.rb +83 -47
- data/lib/treequel/monkeypatches.rb +29 -0
- data/lib/treequel/schema.rb +23 -19
- data/lib/treequel/schema/attributetype.rb +33 -3
- data/lib/treequel/schema/ldapsyntax.rb +0 -11
- data/lib/treequel/schema/matchingrule.rb +0 -11
- data/lib/treequel/schema/matchingruleuse.rb +0 -11
- data/lib/treequel/schema/objectclass.rb +36 -10
- data/lib/treequel/schema/table.rb +159 -0
- data/lib/treequel/sequel_integration.rb +7 -7
- data/lib/treequel/utils.rb +4 -66
- data/rake/documentation.rb +89 -0
- data/rake/helpers.rb +375 -307
- data/rake/hg.rb +16 -2
- data/rake/manual.rb +11 -6
- data/rake/packaging.rb +20 -35
- data/rake/publishing.rb +22 -62
- data/spec/lib/constants.rb +20 -0
- data/spec/lib/control_behavior.rb +44 -0
- data/spec/lib/matchers.rb +51 -0
- data/spec/treequel/branch_spec.rb +88 -29
- data/spec/treequel/branchcollection_spec.rb +24 -1
- data/spec/treequel/branchset_spec.rb +123 -51
- data/spec/treequel/control_spec.rb +48 -0
- data/spec/treequel/controls/contentsync_spec.rb +38 -0
- data/spec/treequel/controls/pagedresults_spec.rb +138 -0
- data/spec/treequel/controls/sortedresults_spec.rb +171 -0
- data/spec/treequel/directory_spec.rb +186 -16
- data/spec/treequel/mixins_spec.rb +42 -3
- data/spec/treequel/schema/attributetype_spec.rb +22 -20
- data/spec/treequel/schema/objectclass_spec.rb +67 -46
- data/spec/treequel/schema/table_spec.rb +134 -0
- data/spec/treequel_spec.rb +277 -15
- metadata +89 -108
- data/bin/treequel.orig +0 -963
- data/examples/ldap-monitor.rb +0 -143
- data/examples/ldap-monitor/public/css/master.css +0 -328
- data/examples/ldap-monitor/public/images/card_small.png +0 -0
- data/examples/ldap-monitor/public/images/chain_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small.png +0 -0
- data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
- data/examples/ldap-monitor/public/images/plug.png +0 -0
- data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
- data/examples/ldap-monitor/public/images/tick.png +0 -0
- data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
- data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
- data/examples/ldap-monitor/views/backends.erb +0 -41
- data/examples/ldap-monitor/views/connections.erb +0 -74
- data/examples/ldap-monitor/views/databases.erb +0 -39
- data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
- data/examples/ldap-monitor/views/index.erb +0 -14
- data/examples/ldap-monitor/views/layout.erb +0 -35
- data/examples/ldap-monitor/views/listeners.erb +0 -30
- data/rake/rdoc.rb +0 -30
- data/rake/win32.rb +0 -190
data/bin/treequel
CHANGED
@@ -4,7 +4,9 @@ require 'rubygems'
|
|
4
4
|
|
5
5
|
require 'abbrev'
|
6
6
|
require 'columnize'
|
7
|
+
require 'diff/lcs'
|
7
8
|
require 'digest/sha1'
|
9
|
+
require 'irb'
|
8
10
|
require 'logger'
|
9
11
|
require 'open3'
|
10
12
|
require 'optparse'
|
@@ -16,6 +18,7 @@ require 'tempfile'
|
|
16
18
|
require 'terminfo'
|
17
19
|
require 'termios'
|
18
20
|
require 'uri'
|
21
|
+
require 'yaml'
|
19
22
|
|
20
23
|
require 'treequel'
|
21
24
|
require 'treequel/mixins'
|
@@ -33,6 +36,41 @@ class OpenStruct
|
|
33
36
|
end
|
34
37
|
|
35
38
|
|
39
|
+
### IRb.start_session, courtesy of Joel VanderWerf in [ruby-talk:42437].
|
40
|
+
require 'irb'
|
41
|
+
require 'irb/completion'
|
42
|
+
|
43
|
+
module IRB # :nodoc:
|
44
|
+
def self.start_session( obj )
|
45
|
+
unless @__initialized
|
46
|
+
args = ARGV
|
47
|
+
ARGV.replace(ARGV.dup)
|
48
|
+
IRB.setup(nil)
|
49
|
+
ARGV.replace(args)
|
50
|
+
@__initialized = true
|
51
|
+
end
|
52
|
+
|
53
|
+
workspace = WorkSpace.new( obj )
|
54
|
+
irb = Irb.new( workspace )
|
55
|
+
|
56
|
+
@CONF[:IRB_RC].call( irb.context ) if @CONF[:IRB_RC]
|
57
|
+
@CONF[:MAIN_CONTEXT] = irb.context
|
58
|
+
|
59
|
+
begin
|
60
|
+
prevhandler = Signal.trap( 'INT' ) do
|
61
|
+
irb.signal_handle
|
62
|
+
end
|
63
|
+
|
64
|
+
catch( :IRB_EXIT ) do
|
65
|
+
irb.eval_input
|
66
|
+
end
|
67
|
+
ensure
|
68
|
+
Signal.trap( 'INT', prevhandler )
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
36
74
|
# The Treequel shell.
|
37
75
|
class Treequel::Shell
|
38
76
|
include Readline,
|
@@ -41,6 +79,8 @@ class Treequel::Shell
|
|
41
79
|
Treequel::Constants::Patterns,
|
42
80
|
Treequel::HashUtilities
|
43
81
|
|
82
|
+
extend Treequel::ANSIColorUtilities
|
83
|
+
|
44
84
|
# Prompt text for #prompt_for_multiple_values
|
45
85
|
MULTILINE_PROMPT = <<-'EOF'
|
46
86
|
Enter one or more values for '%s'.
|
@@ -61,8 +101,11 @@ class Treequel::Shell
|
|
61
101
|
}.freeze
|
62
102
|
LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
|
63
103
|
|
104
|
+
# Valid connect-type arguments
|
105
|
+
VALID_CONNECT_TYPES = %w[tls ssl plain]
|
106
|
+
|
64
107
|
# Command option parsers
|
65
|
-
|
108
|
+
@@option_parsers = {}
|
66
109
|
|
67
110
|
# Path to the default history file
|
68
111
|
HISTORY_FILE = Pathname( "~/.treequel.history" )
|
@@ -75,6 +118,59 @@ class Treequel::Shell
|
|
75
118
|
### C L A S S M E T H O D S
|
76
119
|
#################################################################
|
77
120
|
|
121
|
+
### Run the shell.
|
122
|
+
def self::run( args )
|
123
|
+
Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
|
124
|
+
bind_as, uri = self.parse_options( args )
|
125
|
+
|
126
|
+
directory = if uri
|
127
|
+
Treequel.directory( uri )
|
128
|
+
else
|
129
|
+
Treequel.directory_from_config
|
130
|
+
end
|
131
|
+
|
132
|
+
Treequel::Shell.new( directory ).run( bind_as )
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
### Parse command-line options for shell startup and return an options struct and
|
137
|
+
### the LDAP URI.
|
138
|
+
def self::parse_options( argv )
|
139
|
+
progname = File.basename( $0 )
|
140
|
+
bind_as = nil
|
141
|
+
|
142
|
+
oparser = OptionParser.new( "Usage: #{progname} [OPTIONS] [LDAPURL]" ) do |oparser|
|
143
|
+
oparser.separator ' '
|
144
|
+
|
145
|
+
oparser.on( '--binddn=DN', '-b DN', String, "Bind as DN" ) do |dn|
|
146
|
+
bind_as = dn
|
147
|
+
end
|
148
|
+
|
149
|
+
oparser.on( '--loglevel=LEVEL', '-l LEVEL', Treequel::Loggable::LEVEL.keys,
|
150
|
+
"Set the logging level. Should be one of:",
|
151
|
+
Treequel::Loggable::LEVEL.keys.collect {|lvl| lvl.to_s } ) do |lvl|
|
152
|
+
Treequel.logger.level = Treequel::Loggable::LEVEL[ lvl.to_sym ] or
|
153
|
+
raise "Invalid logging level %p" % [ lvl ]
|
154
|
+
end
|
155
|
+
|
156
|
+
oparser.on( '--debug', '-d', FalseClass, "Turn debugging on" ) do
|
157
|
+
$DEBUG = true
|
158
|
+
$trace = true
|
159
|
+
Treequel.logger.level = Logger::DEBUG
|
160
|
+
end
|
161
|
+
|
162
|
+
oparser.on("-h", "--help", "Show this help message.") do
|
163
|
+
$stderr.puts( oparser )
|
164
|
+
exit!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
remaining_args = oparser.parse( argv )
|
169
|
+
|
170
|
+
return bind_as, *remaining_args
|
171
|
+
end
|
172
|
+
|
173
|
+
|
78
174
|
### Create an option parser from the specified +block+ for the given +command+ and register
|
79
175
|
### it. Many thanks to apeiros and dominikh on #Ruby-Pro for the ideas behind this.
|
80
176
|
def self::set_options( command, &block )
|
@@ -84,7 +180,7 @@ class Treequel::Shell
|
|
84
180
|
end
|
85
181
|
oparser.default_argv = []
|
86
182
|
|
87
|
-
|
183
|
+
@@option_parsers[command.to_sym] = [oparser, options]
|
88
184
|
end
|
89
185
|
|
90
186
|
|
@@ -92,14 +188,12 @@ class Treequel::Shell
|
|
92
188
|
### I N S T A N C E M E T H O D S
|
93
189
|
#################################################################
|
94
190
|
|
95
|
-
### Create a new shell
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
@uri = uri
|
191
|
+
### Create a new shell for the specified +directory+.
|
192
|
+
### @param [Treequel::Directory] directory the LDAP directory to navigate
|
193
|
+
def initialize( directory )
|
194
|
+
@dir = directory
|
195
|
+
@uri = directory.uri
|
101
196
|
@quit = false
|
102
|
-
@dir = Treequel.directory( @uri )
|
103
197
|
@currbranch = @dir
|
104
198
|
@columns = TermInfo.screen_width
|
105
199
|
@rows = TermInfo.screen_height
|
@@ -110,8 +204,24 @@ class Treequel::Shell
|
|
110
204
|
end
|
111
205
|
|
112
206
|
|
207
|
+
######
|
208
|
+
public
|
209
|
+
######
|
210
|
+
|
211
|
+
# The number of columns in the current terminal
|
212
|
+
attr_reader :columns
|
213
|
+
|
214
|
+
# The number of rows in the current terminal
|
215
|
+
attr_reader :rows
|
216
|
+
|
217
|
+
# The flag which causes the shell to exit after the current loop
|
218
|
+
attr_accessor :quit
|
219
|
+
|
220
|
+
|
113
221
|
### The command loop: run the shell until the user wants to quit
|
114
|
-
|
222
|
+
### @param [String] bind_as The DN of the user to bind as. If none is
|
223
|
+
### specified, the shell will bind anonymously.
|
224
|
+
def run( bind_as=nil )
|
115
225
|
@original_tty_settings = IO.read( '|-' ) or exec 'stty', '-g'
|
116
226
|
message "Connected to %s" % [ @uri ]
|
117
227
|
|
@@ -121,6 +231,13 @@ class Treequel::Shell
|
|
121
231
|
# Load saved command-line history
|
122
232
|
self.read_history
|
123
233
|
|
234
|
+
# If the user said to bind as someone on the command line, invoke a
|
235
|
+
# 'bind' command before dropping into the command line
|
236
|
+
if bind_as
|
237
|
+
options = OpenStruct.new
|
238
|
+
self.bind_command( options, bind_as )
|
239
|
+
end
|
240
|
+
|
124
241
|
# Run until something sets the quit flag
|
125
242
|
until @quit
|
126
243
|
$stderr.puts
|
@@ -137,7 +254,7 @@ class Treequel::Shell
|
|
137
254
|
elsif input == ''
|
138
255
|
self.log.debug "No command. Re-displaying the prompt."
|
139
256
|
|
140
|
-
# Parse everything else into command +
|
257
|
+
# Parse everything else into command + args
|
141
258
|
else
|
142
259
|
self.log.debug "Dispatching input: %p" % [ input ]
|
143
260
|
self.dispatch_cmd( input )
|
@@ -164,8 +281,8 @@ class Treequel::Shell
|
|
164
281
|
|
165
282
|
# If there's a registered optionparser for the command, use it to
|
166
283
|
# split out options and arguments, then pass those to the command.
|
167
|
-
if
|
168
|
-
oparser, options =
|
284
|
+
if @@option_parsers.key?( full_command )
|
285
|
+
oparser, options = @@option_parsers[ full_command ]
|
169
286
|
self.log.debug "Got an option-parser for #{full_command}."
|
170
287
|
|
171
288
|
cmdargs = oparser.parse( args )
|
@@ -176,6 +293,7 @@ class Treequel::Shell
|
|
176
293
|
|
177
294
|
# ...otherwise just call it with all the args.
|
178
295
|
else
|
296
|
+
self.log.warn " no options defined for '%s' command" % [ command ]
|
179
297
|
meth.call( *args )
|
180
298
|
end
|
181
299
|
|
@@ -196,6 +314,21 @@ class Treequel::Shell
|
|
196
314
|
protected
|
197
315
|
#########
|
198
316
|
|
317
|
+
### Fetch a Treequel::Directory object for the directory at the given +uri+, or
|
318
|
+
### quit with an error if unable to do so.
|
319
|
+
def get_ldap_directory( uri, options )
|
320
|
+
if uri.port == LDAP::LDAP_PORT
|
321
|
+
if options.try_tls
|
322
|
+
return Treequel.directory( uri, :connect_type => :tls )
|
323
|
+
else
|
324
|
+
return Treequel.directory( uri, :connect_type => :plain )
|
325
|
+
end
|
326
|
+
else
|
327
|
+
return Treequel.directory( uri, :connect_type => :ssl )
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
|
199
332
|
### Set up Readline completion
|
200
333
|
def setup_completion
|
201
334
|
Readline.completion_proc = self.method( :completion_callback ).to_proc
|
@@ -210,7 +343,7 @@ class Treequel::Shell
|
|
210
343
|
|
211
344
|
if histfile.exist?
|
212
345
|
lines = histfile.readlines.collect {|line| line.chomp }
|
213
|
-
self.log.debug "Read %d saved history commands from %s." % [ lines.
|
346
|
+
self.log.debug "Read %d saved history commands from %s." % [ lines.length, histfile ]
|
214
347
|
Readline::HISTORY.push( *lines )
|
215
348
|
else
|
216
349
|
self.log.debug "History file '%s' was empty or non-existant." % [ histfile ]
|
@@ -224,7 +357,7 @@ class Treequel::Shell
|
|
224
357
|
|
225
358
|
lines = Readline::HISTORY.to_a.reverse.uniq.reverse
|
226
359
|
lines = lines[ -DEFAULT_HISTORY_SIZE, DEFAULT_HISTORY_SIZE ] if
|
227
|
-
lines.
|
360
|
+
lines.length > DEFAULT_HISTORY_SIZE
|
228
361
|
|
229
362
|
self.log.debug "Saving %d history lines to %s." % [ lines.length, histfile ]
|
230
363
|
|
@@ -240,7 +373,11 @@ class Treequel::Shell
|
|
240
373
|
parts = Shellwords.shellwords( input )
|
241
374
|
|
242
375
|
# If there aren't any arguments, it's command completion
|
243
|
-
if parts.
|
376
|
+
if parts.empty?
|
377
|
+
possible_completions = @commands.sort
|
378
|
+
self.log.debug " possible completions: %p" % [ possible_completions ]
|
379
|
+
return possible_completions
|
380
|
+
elsif parts.length == 1
|
244
381
|
# One completion means it's an unambiguous match, so just complete it.
|
245
382
|
possible_completions = @commands.grep( /^#{Regexp.quote(input)}/ ).sort
|
246
383
|
self.log.debug " possible completions: %p" % [ possible_completions ]
|
@@ -273,15 +410,15 @@ class Treequel::Shell
|
|
273
410
|
|
274
411
|
### Show help text for the specified command, or a list of all available commands
|
275
412
|
### if none is specified.
|
276
|
-
def help_command( *args )
|
413
|
+
def help_command( options, *args )
|
277
414
|
if args.empty?
|
278
415
|
$stderr.puts
|
279
416
|
message colorize( "Available commands", :bold, :white ),
|
280
417
|
*columnize(@commands)
|
281
418
|
else
|
282
419
|
cmd = args.shift.to_sym
|
283
|
-
if
|
284
|
-
oparser, _ =
|
420
|
+
if @@option_parsers.key?( cmd )
|
421
|
+
oparser, _ = @@option_parsers[ cmd ]
|
285
422
|
self.log.debug "Setting summary width to: %p" % [ @columns ]
|
286
423
|
oparser.summary_width = @columns
|
287
424
|
output = oparser.to_s.sub( /^(.*?)\n/ ) do |match|
|
@@ -291,22 +428,30 @@ class Treequel::Shell
|
|
291
428
|
$stderr.puts
|
292
429
|
message( output )
|
293
430
|
else
|
294
|
-
error_message( "No help for '#{cmd}'" )
|
431
|
+
error_message( "No help for '#{cmd.inspect}'" )
|
295
432
|
end
|
296
433
|
end
|
297
434
|
end
|
435
|
+
set_options :help do |oparser, options|
|
436
|
+
oparser.banner = "help [COMMAND]"
|
437
|
+
oparser.separator 'Display general help, or help for a specific COMMAND.'
|
438
|
+
end
|
298
439
|
|
299
440
|
|
300
441
|
### Quit the shell.
|
301
|
-
def quit_command( *args )
|
442
|
+
def quit_command( options, *args )
|
302
443
|
message "Okay, exiting."
|
303
|
-
|
444
|
+
self.quit = true
|
445
|
+
end
|
446
|
+
set_options :help do |oparser, options|
|
447
|
+
oparser.banner = "quit"
|
448
|
+
oparser.separator 'Exit the shell.'
|
304
449
|
end
|
305
450
|
|
306
451
|
|
307
452
|
### Set the logging level (if invoked with an argument) or display the current
|
308
453
|
### level (with no argument).
|
309
|
-
def log_command( *args )
|
454
|
+
def log_command( options, *args )
|
310
455
|
newlevel = args.shift
|
311
456
|
if newlevel
|
312
457
|
if LOG_LEVELS.key?( newlevel )
|
@@ -321,24 +466,49 @@ class Treequel::Shell
|
|
321
466
|
[ LOG_LEVEL_NAMES[Treequel.logger.level] ]
|
322
467
|
end
|
323
468
|
end
|
469
|
+
set_options :log do |oparser, options|
|
470
|
+
oparser.banner = "log [LEVEL]"
|
471
|
+
oparser.separator 'Set the logging level, or display the current level if no level ' +
|
472
|
+
"is given. Valid log levels are: %s" %
|
473
|
+
LOG_LEVEL_NAMES.keys.sort.join(', ')
|
474
|
+
end
|
324
475
|
|
325
476
|
|
326
477
|
### Display LDIF for the specified RDNs.
|
327
|
-
def cat_command( *args )
|
478
|
+
def cat_command( options, *args )
|
328
479
|
args.each do |rdn|
|
480
|
+
extended = rdn.chomp!( '+' )
|
481
|
+
|
329
482
|
branch = @currbranch.get_child( rdn )
|
330
|
-
|
483
|
+
branch.include_operational_attrs = true if extended
|
484
|
+
|
485
|
+
if branch.exists?
|
486
|
+
ldifstring = branch.to_ldif( self.columns - 2 )
|
487
|
+
self.log.debug "LDIF: #{ldifstring.dump}"
|
488
|
+
|
489
|
+
message( format_ldif(ldifstring) )
|
490
|
+
else
|
491
|
+
error_message( "No such entry %s" % [branch.dn] )
|
492
|
+
end
|
331
493
|
end
|
332
494
|
end
|
495
|
+
set_options :cat do |oparser, options|
|
496
|
+
oparser.banner = "cat [RDN]+"
|
497
|
+
oparser.separator 'Display the entries specified by RDN as LDIF.'
|
498
|
+
end
|
333
499
|
|
334
500
|
|
335
501
|
### Display YAML for the specified RDNs.
|
336
|
-
def yaml_command( *args )
|
502
|
+
def yaml_command( options, *args )
|
337
503
|
args.each do |rdn|
|
338
504
|
branch = @currbranch.get_child( rdn )
|
339
505
|
message( branch_as_yaml(branch) )
|
340
506
|
end
|
341
507
|
end
|
508
|
+
set_options :yaml do |oparser, options|
|
509
|
+
oparser.banner = "yaml [RDN]+"
|
510
|
+
oparser.separator 'Display the entries specified by RDN as YAML.'
|
511
|
+
end
|
342
512
|
|
343
513
|
|
344
514
|
### List the children of the branch specified by the given +rdn+, or the current branch if none
|
@@ -371,68 +541,70 @@ class Treequel::Shell
|
|
371
541
|
end
|
372
542
|
end
|
373
543
|
set_options :ls do |oparser, options|
|
374
|
-
oparser.banner = "ls [OPTIONS] [
|
544
|
+
oparser.banner = "ls [OPTIONS] [DN]+"
|
545
|
+
oparser.separator 'List the entries specified, or the current entry if none are specified.'
|
546
|
+
oparser.separator ''
|
375
547
|
|
376
548
|
oparser.on( "-l", "--long", FalseClass, "List in long format." ) do
|
377
549
|
options.longform = true
|
378
550
|
end
|
551
|
+
oparser.on( "-t", "--timesort", FalseClass,
|
552
|
+
"Sort by time modified (most recently modified first)." ) do
|
553
|
+
options.timesort = true
|
554
|
+
end
|
555
|
+
oparser.on( "-d", "--dirsort", FalseClass,
|
556
|
+
"Sort entries with subordinate entries before those without." ) do
|
557
|
+
options.dirsort = true
|
558
|
+
end
|
559
|
+
oparser.on( "-r", "--reverse", FalseClass, "Reverse the entry sort functions." ) do
|
560
|
+
options.reversesort = true
|
561
|
+
end
|
379
562
|
|
380
563
|
end
|
381
564
|
|
382
565
|
|
383
566
|
### Generate long-form output lines for the 'ls' command for the given +branch+.
|
384
567
|
def make_longform_ls_output( branch, options )
|
385
|
-
rows = []
|
386
568
|
children = branch.children
|
387
|
-
|
569
|
+
header = colorize( :underscore, :cyan ) { "total %d" % [children.length] }
|
388
570
|
|
389
571
|
# Calcuate column widths
|
390
|
-
oclen = children.map do |
|
391
|
-
|
392
|
-
|
393
|
-
end.max
|
394
|
-
moddnlen = children.map do |branch|
|
395
|
-
branch[:modifiersName].length
|
572
|
+
oclen = children.map do |subbranch|
|
573
|
+
subbranch.include_operational_attrs = true
|
574
|
+
subbranch[:structuralObjectClass].length
|
396
575
|
end.max
|
397
576
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
# hasSubordinates: TRUE
|
405
|
-
# modifyTimestamp: 20090520232650Z
|
406
|
-
# structuralObjectClass: organizationalUnit
|
407
|
-
rows << "%#{oclen}s %#{moddnlen}s %s %s%s" % [
|
408
|
-
branch[:structuralObjectClass],
|
409
|
-
branch[:modifiersName],
|
410
|
-
branch[:modifyTimestamp].strftime('%Y-%m-%d %H:%M'),
|
411
|
-
format_rdn( branch.rdn ),
|
412
|
-
branch[:hasSubordinates] ? '/' : ''
|
413
|
-
]
|
414
|
-
end
|
577
|
+
# Set up sorting by collecting all the requested sort criteria as Proc objects which
|
578
|
+
# will be applied
|
579
|
+
sortfuncs = []
|
580
|
+
sortfuncs << lambda {|subbranch| subbranch[:hasSubordinates] ? 0 : 1 } if options.dirsort
|
581
|
+
sortfuncs << lambda {|subbranch| subbranch[:modifyTimestamp] } if options.timesort
|
582
|
+
sortfuncs << lambda {|subbranch| subbranch.rdn.downcase }
|
415
583
|
|
416
|
-
|
584
|
+
rows = children.
|
585
|
+
sort_by {|subbranch| sortfuncs.collect {|func| func.call(subbranch) } }.
|
586
|
+
collect {|subbranch| self.format_description(subbranch, oclen) }
|
587
|
+
|
588
|
+
return [ header ] + (options.reversesort ? rows.reverse : rows)
|
417
589
|
end
|
418
590
|
|
419
591
|
|
420
592
|
### Generate short-form 'ls' output for the given +branch+ and return it.
|
421
593
|
def make_shortform_ls_output( branch, options )
|
594
|
+
branch.include_operational_attrs = true
|
422
595
|
entries = branch.children.
|
423
|
-
collect {|b| b.rdn }.
|
596
|
+
collect {|b| b.rdn + (b[:hasSubordinates] ? '/' : '') }.
|
424
597
|
sort_by {|rdn| rdn.downcase }
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
end
|
598
|
+
self.log.debug "Displaying %d entries in short form." % [ entries.length ]
|
599
|
+
|
600
|
+
return columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
|
601
|
+
format_rdn( rdn )
|
602
|
+
end
|
431
603
|
end
|
432
604
|
|
433
605
|
|
434
606
|
### Change the current working DN to +rdn+.
|
435
|
-
def cdn_command( rdn=nil, *args )
|
607
|
+
def cdn_command( options, rdn=nil, *args )
|
436
608
|
if rdn.nil?
|
437
609
|
@currbranch = @dir.base
|
438
610
|
return
|
@@ -446,64 +618,91 @@ class Treequel::Shell
|
|
446
618
|
pairs.each do |dnpair|
|
447
619
|
self.log.debug " cd to %p" % [ dnpair ]
|
448
620
|
attribute, value = dnpair.split( /=/, 2 )
|
449
|
-
self.log.debug " changing to %s( %p )" % [ attribute, value ]
|
450
|
-
@currbranch = @currbranch.send( attribute, value )
|
621
|
+
self.log.debug " changing to %s( %p )" % [ attribute.downcase, value ]
|
622
|
+
@currbranch = @currbranch.send( attribute.downcase, value )
|
451
623
|
end
|
452
624
|
end
|
625
|
+
set_options :cdn do |oparser, options|
|
626
|
+
oparser.banner = "cdn <RDN>"
|
627
|
+
oparser.separator 'Change the current entry to <RDN>.'
|
628
|
+
end
|
453
629
|
|
454
630
|
|
455
631
|
### Change the current working DN to the current entry's parent.
|
456
|
-
def parent_command( *args )
|
632
|
+
def parent_command( options, *args )
|
457
633
|
parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]
|
458
634
|
|
459
635
|
self.log.debug " changing to %s" % [ parent.dn ]
|
460
636
|
@currbranch = parent
|
461
637
|
end
|
638
|
+
set_options :parent do |oparser, options|
|
639
|
+
oparser.banner = "parent"
|
640
|
+
oparser.separator "Change to the current entry's parent."
|
641
|
+
end
|
642
|
+
|
643
|
+
|
644
|
+
# ### Create the entry specified by +rdn+.
|
645
|
+
def create_command( options, rdn )
|
646
|
+
branch = @currbranch.get_child( rdn )
|
647
|
+
|
648
|
+
raise "#{branch.dn}: already exists." if branch.exists?
|
649
|
+
create_new_entry( branch )
|
650
|
+
end
|
651
|
+
set_options :create do |oparser, options|
|
652
|
+
oparser.banner = "create <RDN>"
|
653
|
+
oparser.separator "Create a new entry at <RDN>."
|
654
|
+
end
|
462
655
|
|
463
656
|
|
464
657
|
### Edit the entry specified by +rdn+.
|
465
|
-
|
466
|
-
|
467
|
-
# entryhash = nil
|
468
|
-
#
|
469
|
-
# if options.newentry
|
470
|
-
# raise "#{branch.dn} already exists." if branch.exists?
|
471
|
-
# object_classes = prompt_for_multiple_values( "Entry objectClasses:" )
|
472
|
-
# entryhash = branch.valid_attributes_hash( *object_classes )
|
473
|
-
# newhash = edit_in_yaml( entryhash )
|
474
|
-
# args = object_classes + [newhash]
|
475
|
-
# branch.create( *args )
|
476
|
-
# else
|
477
|
-
# raise "#{branch.dn}: no such entry. Did you mean to create it with -n?" unless
|
478
|
-
# branch.exists?
|
479
|
-
# entryhash = branch.entry
|
480
|
-
# newhash = edit_in_yaml( entryhash )
|
481
|
-
# branch.merge( entryhash )
|
482
|
-
# end
|
483
|
-
#
|
484
|
-
# message "Saved #{rdn}."
|
485
|
-
#end
|
486
|
-
#set_options :edit do |oparser, options|
|
487
|
-
# oparser.banner = "edit [OPTIONS] DN"
|
488
|
-
#
|
489
|
-
# oparser.on( "-n", "--new", FalseClass,
|
490
|
-
# "Create a new entry instead of editing an existing one." ) do
|
491
|
-
# options.newentry = true
|
492
|
-
# end
|
493
|
-
#
|
494
|
-
#end
|
658
|
+
def edit_command( options, rdn )
|
659
|
+
branch = @currbranch.get_child( rdn )
|
495
660
|
|
661
|
+
raise "#{branch.dn}: no such entry. Did you mean to 'create' it instead? " unless
|
662
|
+
branch.exists?
|
496
663
|
|
497
|
-
|
498
|
-
|
499
|
-
### branchsets that will find matching entries.
|
500
|
-
def convert_to_branchsets( *patterns )
|
501
|
-
self.log.debug "Turning %d patterns into branchsets." % [ patterns.length ]
|
502
|
-
return patterns.collect do |pat|
|
503
|
-
key, val = pat.split( /\s*=\s*/, 2 )
|
504
|
-
self.log.debug " making a filter out of %p => %p" % [ key, val ]
|
505
|
-
@currbranch.filter( key => val )
|
664
|
+
if entryhash = edit_in_yaml( branch )
|
665
|
+
branch.merge( entryhash )
|
506
666
|
end
|
667
|
+
|
668
|
+
message "Saved #{rdn}."
|
669
|
+
end
|
670
|
+
set_options :edit do |oparser, options|
|
671
|
+
oparser.banner = "edit <RDN>"
|
672
|
+
oparser.separator "Edit the entry at RDN as YAML."
|
673
|
+
end
|
674
|
+
|
675
|
+
|
676
|
+
### Change the DN of an entry
|
677
|
+
def mv_command( options, rdn, newdn )
|
678
|
+
branch = @currbranch.get_child( rdn )
|
679
|
+
|
680
|
+
raise "#{branch.dn}: no such entry" unless branch.exists?
|
681
|
+
olddn = branch.dn
|
682
|
+
branch.move( newdn )
|
683
|
+
message " %s -> %s: success" % [ olddn, branch.dn ]
|
684
|
+
end
|
685
|
+
set_options :mv do |oparser, options|
|
686
|
+
oparser.banner = "mv <RDN> <NEWRDN>"
|
687
|
+
oparser.separator "Move the entry at RDN to NEWRDN"
|
688
|
+
end
|
689
|
+
|
690
|
+
|
691
|
+
### Copy an entry
|
692
|
+
def cp_command( options, rdn, newrdn )
|
693
|
+
branch = @currbranch.get_child( rdn )
|
694
|
+
newbranch = @currbranch.get_child( newrdn )
|
695
|
+
|
696
|
+
raise "#{branch.dn}: already exists" if newbranch.exists?
|
697
|
+
|
698
|
+
attributes = branch.entry.merge( :dn => newbranch.dn )
|
699
|
+
newbranch.create( attributes )
|
700
|
+
|
701
|
+
message " %s -> %s: success" % [ rdn, branch.dn ]
|
702
|
+
end
|
703
|
+
set_options :cp do |oparser, options|
|
704
|
+
oparser.banner = "cp <RDN> <NEWRDN>"
|
705
|
+
oparser.separator "Copy the entry at RDN to a new entry at NEWRDN"
|
507
706
|
end
|
508
707
|
|
509
708
|
|
@@ -515,17 +714,29 @@ class Treequel::Shell
|
|
515
714
|
branches = coll.all
|
516
715
|
|
517
716
|
msg = "About to delete the following entries:\n" +
|
518
|
-
branches.collect {|br|
|
717
|
+
columnize( branches.collect {|br| br.dn } )
|
519
718
|
|
520
|
-
|
521
|
-
branches.each do |
|
522
|
-
|
523
|
-
message "
|
719
|
+
if options.force
|
720
|
+
branches.each do |br|
|
721
|
+
br.directory.delete( br )
|
722
|
+
message " delete %s: success" % [ br.dn ]
|
723
|
+
end
|
724
|
+
else
|
725
|
+
ask_for_confirmation( msg ) do
|
726
|
+
branches.each do |br|
|
727
|
+
br.directory.delete( br )
|
728
|
+
message " delete %s: success" % [ br.dn ]
|
729
|
+
end
|
524
730
|
end
|
525
731
|
end
|
526
732
|
end
|
527
733
|
set_options :rm do |oparser, options|
|
528
|
-
oparser.banner = "rm
|
734
|
+
oparser.banner = "rm <RDN>+"
|
735
|
+
oparser.separator 'Remove the entries at the given RDNs.'
|
736
|
+
|
737
|
+
oparser.on( '-f', '--force', TrueClass, "Force -- remove without confirmation." ) do
|
738
|
+
options.force = true
|
739
|
+
end
|
529
740
|
end
|
530
741
|
|
531
742
|
|
@@ -538,16 +749,32 @@ class Treequel::Shell
|
|
538
749
|
message "Searching for entries that match '#{branchset.to_s}'"
|
539
750
|
|
540
751
|
entries = branchset.all
|
541
|
-
output = columnize( entries ).
|
542
|
-
|
543
|
-
|
544
|
-
format_rdn( rdn )
|
545
|
-
end
|
546
|
-
end
|
752
|
+
output = columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
|
753
|
+
format_rdn( rdn )
|
754
|
+
end
|
547
755
|
message( output )
|
548
756
|
end
|
549
757
|
set_options :grep do |oparser, options|
|
550
|
-
oparser.banner = "grep [OPTIONS] FILTER"
|
758
|
+
oparser.banner = "grep [OPTIONS] <FILTER>"
|
759
|
+
oparser.separator 'Search for children of the current entry that match the given FILTER'
|
760
|
+
|
761
|
+
oparser.on( '-r', '--recursive', TrueClass, "Search recursively." ) do
|
762
|
+
options.force = true
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
|
767
|
+
### Show who the shell is currently bound as.
|
768
|
+
def whoami_command( options, *args )
|
769
|
+
if user = @dir.bound_user
|
770
|
+
message "Bound as #{user}"
|
771
|
+
else
|
772
|
+
message "Bound anonymously"
|
773
|
+
end
|
774
|
+
end
|
775
|
+
set_options :whoami do |oparser, options|
|
776
|
+
oparser.banner = "whoami"
|
777
|
+
oparser.separator 'Display the DN of the user the shell is bound as.'
|
551
778
|
end
|
552
779
|
|
553
780
|
|
@@ -564,17 +791,37 @@ class Treequel::Shell
|
|
564
791
|
else
|
565
792
|
user = @dir.filter( :uid => binddn ).first
|
566
793
|
end
|
567
|
-
raise "No user found for %p" % [ binddn ] unless user.exists?
|
568
794
|
|
569
795
|
@dir.bind( user, password )
|
570
796
|
message "Bound as #{user}"
|
571
797
|
end
|
572
798
|
set_options :bind do |oparser, options|
|
573
799
|
oparser.banner = "bind [BIND_DN or UID]"
|
800
|
+
oparser.separator "Bind as BIND_DN or UID"
|
574
801
|
oparser.separator "If you don't specify a BIND_DN, you will be prompted for it."
|
575
802
|
end
|
576
803
|
|
577
804
|
|
805
|
+
### Start an IRB session on either the current branchset, if invoked with no arguments, or
|
806
|
+
### on a branchset for the specified +rdn+ if one is given.
|
807
|
+
def irb_command( options, *args )
|
808
|
+
branch = nil
|
809
|
+
if args.empty?
|
810
|
+
branch = @currbranch
|
811
|
+
else
|
812
|
+
branch = @currbranch.get_child( args.first )
|
813
|
+
end
|
814
|
+
|
815
|
+
self.log.debug "Setting up IRb shell"
|
816
|
+
IRB.start_session( branch )
|
817
|
+
end
|
818
|
+
set_options :irb do |oparser, options|
|
819
|
+
oparser.banner = "irb [RDN]"
|
820
|
+
oparser.separator "Start an IRb shell with either the current branch (if none is " +
|
821
|
+
"specified) or a branch for the entry specified by the given RDN."
|
822
|
+
end
|
823
|
+
|
824
|
+
|
578
825
|
### Handle a command from the user that doesn't exist.
|
579
826
|
def handle_missing_cmd( *args )
|
580
827
|
command = args.shift || '(testing?)'
|
@@ -593,18 +840,84 @@ class Treequel::Shell
|
|
593
840
|
end
|
594
841
|
|
595
842
|
|
596
|
-
|
597
|
-
|
598
|
-
|
843
|
+
### Convert the given +patterns+ to branchsets relative to the current branch and return
|
844
|
+
### them. This is used to map shell arguments like 'cn=*', 'Hosts', 'cn=dav*' into
|
845
|
+
### branchsets that will find matching entries.
|
846
|
+
def convert_to_branchsets( *patterns )
|
847
|
+
self.log.debug "Turning %d patterns into branchsets." % [ patterns.length ]
|
848
|
+
return patterns.collect do |pat|
|
849
|
+
key, val = pat.split( /\s*=\s*/, 2 )
|
850
|
+
self.log.debug " making a filter out of %p => %p" % [ key, val ]
|
851
|
+
@currbranch.filter( key => val )
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
#################################################################
|
857
|
+
### U T I L I T Y M E T H O D S
|
858
|
+
#################################################################
|
859
|
+
|
860
|
+
### Return the description of the specified +branch+ suitable for displaying in
|
861
|
+
### the directory listing.
|
862
|
+
### @param [Treequel::Branch] branch the branch to be described
|
863
|
+
### @param [Fixnum] oclen the length of the largest objectclass that
|
864
|
+
### will be displayed; used to calculate the
|
865
|
+
### width of the objectclass column.
|
866
|
+
def format_description( branch, oclen=40 )
|
867
|
+
rdn = format_rdn( branch.rdn )
|
868
|
+
metadatalen = oclen + 16 + 6 # oc + timestamp + whitespace
|
869
|
+
maxdesclen = self.columns - metadatalen - rdn.length - 5
|
870
|
+
|
871
|
+
return "%#{oclen}s %s %s%s %s" % [
|
872
|
+
branch[:structuralObjectClass],
|
873
|
+
branch[:modifyTimestamp].strftime('%Y-%m-%d %H:%M'),
|
874
|
+
rdn,
|
875
|
+
branch[:hasSubordinates] ? '/' : '',
|
876
|
+
single_line_description( branch, maxdesclen )
|
877
|
+
]
|
878
|
+
end
|
879
|
+
|
880
|
+
|
881
|
+
### Generate a single-line description from the specified +branch+
|
882
|
+
def single_line_description( branch, maxlen=80 )
|
883
|
+
return '' unless branch[:description] && branch[:description].first
|
884
|
+
desc = branch[:description].join('; ').gsub( /\n+/, '' )
|
885
|
+
desc[ maxlen..desc.length ] = '...' if desc.length > maxlen
|
886
|
+
return '(' + desc + ')'
|
887
|
+
end
|
888
|
+
|
889
|
+
|
890
|
+
### Create a new entry in the directory for the specified +branch+.
|
891
|
+
def create_new_entry( branch )
|
892
|
+
raise "#{branch.dn} already exists." if branch.exists?
|
893
|
+
|
894
|
+
# Prompt for the list of included objectClasses and build the appropriate
|
895
|
+
# blank entry with them in mind.
|
896
|
+
completions = branch.directory.schema.object_classes.keys.collect {|oid| oid.to_s }
|
897
|
+
self.log.debug "Prompting for new entry object classes with %d completions." %
|
898
|
+
[ completions.length ]
|
899
|
+
object_classes = prompt_for_multiple_values( "Entry objectClasses:", nil, completions ).
|
900
|
+
collect {|arg| arg.strip }.compact
|
901
|
+
self.log.debug " user wants %d objectclasses: %p" % [ object_classes.length, object_classes ]
|
902
|
+
|
903
|
+
# Edit the entry
|
904
|
+
if newhash = edit_in_yaml( branch, object_classes )
|
905
|
+
branch.create( newhash )
|
906
|
+
message "Saved #{branch.dn}."
|
907
|
+
else
|
908
|
+
error_message "#{branch.dn} not saved."
|
909
|
+
end
|
910
|
+
end
|
911
|
+
|
599
912
|
|
600
913
|
### Dump the specified +object+ to a file as YAML, invoke an editor on it, then undump the
|
601
914
|
### result. If the file has changed, return the updated object, else returns +nil+.
|
602
|
-
def edit_in_yaml( object )
|
603
|
-
yaml = branch_as_yaml( object )
|
915
|
+
def edit_in_yaml( object, object_classes=[] )
|
916
|
+
yaml = branch_as_yaml( object, false, object_classes )
|
604
917
|
filename = Digest::SHA1.hexdigest( yaml )
|
605
918
|
tempfile = Tempfile.new( filename )
|
606
919
|
|
607
|
-
|
920
|
+
self.log.debug "Object as YAML is: %p" % [ yaml ]
|
608
921
|
tempfile.print( yaml )
|
609
922
|
tempfile.close
|
610
923
|
|
@@ -620,20 +933,26 @@ class Treequel::Shell
|
|
620
933
|
|
621
934
|
|
622
935
|
### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
|
623
|
-
### include the entry's operational attributes.
|
624
|
-
|
936
|
+
### include the entry's operational attributes. If +extra_objectclasses+ contains
|
937
|
+
### one or more objectClass OIDs, include their MUST and MAY attributes when building the
|
938
|
+
### YAML representation of the branch.
|
939
|
+
def branch_as_yaml( object, include_operational=false, extra_objectclasses=[] )
|
625
940
|
object.include_operational_attrs = include_operational
|
626
941
|
|
627
942
|
# Make sure the displayed entry has the MUST attributes
|
628
|
-
entryhash = stringify_keys( object.must_attributes_hash )
|
629
|
-
entryhash.merge!( object.entry )
|
630
|
-
|
631
|
-
|
943
|
+
entryhash = stringify_keys( object.must_attributes_hash(*extra_objectclasses) )
|
944
|
+
entryhash.merge!( object.entry || {} )
|
945
|
+
entryhash.merge!( object.rdn_attributes )
|
946
|
+
entryhash['objectClass'] ||= []
|
947
|
+
entryhash['objectClass'] |= extra_objectclasses
|
948
|
+
|
949
|
+
entryhash.delete( 'dn' ) # Special attribute, can't be edited
|
950
|
+
|
632
951
|
yaml = entryhash.to_yaml
|
633
|
-
yaml[ 5, 0 ] = "# #{dn}\n"
|
634
|
-
|
952
|
+
yaml[ 5, 0 ] = "# #{object.dn}\n"
|
953
|
+
|
635
954
|
# Make comments out of MAY attributes that are unset
|
636
|
-
mayhash = stringify_keys( object.may_attributes_hash )
|
955
|
+
mayhash = stringify_keys( object.may_attributes_hash(*extra_objectclasses) )
|
637
956
|
self.log.debug "MAY hash is: %p" % [ mayhash ]
|
638
957
|
mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
|
639
958
|
yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
|
@@ -655,84 +974,6 @@ class Treequel::Shell
|
|
655
974
|
end
|
656
975
|
|
657
976
|
|
658
|
-
### Return the specified args as a string, quoting any that have a space.
|
659
|
-
def quotelist( *args )
|
660
|
-
return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
|
661
|
-
end
|
662
|
-
|
663
|
-
|
664
|
-
### Run the specified command +cmd+ with system(), failing if the execution
|
665
|
-
### fails.
|
666
|
-
def run_command( *cmd )
|
667
|
-
cmd.flatten!
|
668
|
-
|
669
|
-
if cmd.length > 1
|
670
|
-
self.log.debug( quotelist(*cmd) )
|
671
|
-
else
|
672
|
-
self.log.debug( cmd )
|
673
|
-
end
|
674
|
-
|
675
|
-
if $dryrun
|
676
|
-
self.log.error "(dry run mode)"
|
677
|
-
else
|
678
|
-
system( *cmd )
|
679
|
-
unless $?.success?
|
680
|
-
raise "Command failed: [%s]" % [cmd.join(' ')]
|
681
|
-
end
|
682
|
-
end
|
683
|
-
end
|
684
|
-
|
685
|
-
|
686
|
-
### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
|
687
|
-
### return anything written to its STDOUT.
|
688
|
-
def read_command_output( cmd, *args )
|
689
|
-
self.log.debug "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
|
690
|
-
output = IO.read( '|-' ) or exec cmd, *args
|
691
|
-
return output
|
692
|
-
end
|
693
|
-
|
694
|
-
|
695
|
-
### Run a subordinate Rake process with the same options and the specified +targets+.
|
696
|
-
def rake( *targets )
|
697
|
-
opts = ARGV.select {|arg| arg[0,1] == '-' }
|
698
|
-
args = opts + targets.map {|t| t.to_s }
|
699
|
-
run 'rake', '-N', *args
|
700
|
-
end
|
701
|
-
|
702
|
-
|
703
|
-
### Open a pipe to a process running the given +cmd+ and call the given block with it.
|
704
|
-
def pipeto( *cmd )
|
705
|
-
$DEBUG = true
|
706
|
-
|
707
|
-
cmd.flatten!
|
708
|
-
self.log.info( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
709
|
-
if $dryrun
|
710
|
-
message "(dry run mode)"
|
711
|
-
else
|
712
|
-
open( '|-', 'w+' ) do |io|
|
713
|
-
|
714
|
-
# Parent
|
715
|
-
if io
|
716
|
-
yield( io )
|
717
|
-
|
718
|
-
# Child
|
719
|
-
else
|
720
|
-
exec( *cmd )
|
721
|
-
raise "Command failed: [%s]" % [cmd.join(' ')]
|
722
|
-
end
|
723
|
-
end
|
724
|
-
end
|
725
|
-
end
|
726
|
-
|
727
|
-
|
728
|
-
### Return the fully-qualified path to the specified +program+ in the PATH.
|
729
|
-
def which( program )
|
730
|
-
ENV['PATH'].split(/:/).
|
731
|
-
collect {|dir| Pathname.new(dir) + program }.
|
732
|
-
find {|path| path.exist? && path.executable? }
|
733
|
-
end
|
734
|
-
|
735
|
-
|
736
977
|
### Output the specified message +parts+.
|
737
978
|
def message( *parts )
|
738
979
|
$stderr.puts( *parts )
|
@@ -805,25 +1046,37 @@ class Treequel::Shell
|
|
805
1046
|
|
806
1047
|
|
807
1048
|
### Prompt for an array of values
|
808
|
-
def prompt_for_multiple_values( label, default=nil )
|
809
|
-
|
810
|
-
|
1049
|
+
def prompt_for_multiple_values( label, default=nil, completions=[] )
|
1050
|
+
old_completion_proc = nil
|
1051
|
+
|
1052
|
+
message( MULTILINE_PROMPT % [label] )
|
1053
|
+
if default
|
811
1054
|
message "Enter a single blank line to keep the default:\n %p" % [ default ]
|
812
1055
|
end
|
813
1056
|
|
814
|
-
|
815
|
-
|
1057
|
+
results = []
|
1058
|
+
result = nil
|
1059
|
+
|
1060
|
+
if !completions.empty?
|
1061
|
+
self.log.debug "Prompting with %d completions." % [ completions.length ]
|
1062
|
+
old_completion_proc = Readline.completion_proc
|
1063
|
+
Readline.completion_proc = Proc.new do |input|
|
1064
|
+
completions.flatten.grep( /^#{Regexp.quote(input)}/i ).sort
|
1065
|
+
end
|
1066
|
+
end
|
816
1067
|
|
817
|
-
|
818
|
-
|
1068
|
+
begin
|
1069
|
+
result = readline( make_prompt_string("> ") )
|
819
1070
|
if result.nil? || result.empty?
|
820
1071
|
results << default if default && results.empty?
|
821
1072
|
else
|
822
|
-
|
1073
|
+
results << result
|
823
1074
|
end
|
824
|
-
|
1075
|
+
end until result.nil? || result.empty?
|
825
1076
|
|
826
|
-
|
1077
|
+
return results.flatten
|
1078
|
+
ensure
|
1079
|
+
Readline.completion_proc = old_completion_proc if old_completion_proc
|
827
1080
|
end
|
828
1081
|
|
829
1082
|
|
@@ -884,43 +1137,6 @@ class Treequel::Shell
|
|
884
1137
|
alias :prompt_for_confirmation :ask_for_confirmation
|
885
1138
|
|
886
1139
|
|
887
|
-
### Search line-by-line in the specified +file+ for the given +regexp+, returning the
|
888
|
-
### first match, or nil if no match was found. If the +regexp+ has any capture groups,
|
889
|
-
### those will be returned in an Array, else the whole matching line is returned.
|
890
|
-
def find_pattern_in_file( regexp, file )
|
891
|
-
rval = nil
|
892
|
-
|
893
|
-
File.open( file, 'r' ).each do |line|
|
894
|
-
if (( match = regexp.match(line) ))
|
895
|
-
rval = match.captures.empty? ? match[0] : match.captures
|
896
|
-
break
|
897
|
-
end
|
898
|
-
end
|
899
|
-
|
900
|
-
return rval
|
901
|
-
end
|
902
|
-
|
903
|
-
|
904
|
-
### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
|
905
|
-
### returning the first match, or nil if no match was found. If the +regexp+ has any
|
906
|
-
### capture groups, those will be returned in an Array, else the whole matching line
|
907
|
-
### is returned.
|
908
|
-
def find_pattern_in_pipe( regexp, *cmd )
|
909
|
-
output = []
|
910
|
-
|
911
|
-
self.log.info( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
|
912
|
-
Open3.popen3( *cmd ) do |stdin, stdout, stderr|
|
913
|
-
stdin.close
|
914
|
-
|
915
|
-
output << stdout.gets until stdout.eof?
|
916
|
-
output << stderr.gets until stderr.eof?
|
917
|
-
end
|
918
|
-
|
919
|
-
result = output.find { |line| regexp.match(line) }
|
920
|
-
return $1 || result
|
921
|
-
end
|
922
|
-
|
923
|
-
|
924
1140
|
### Invoke the user's editor on the given +filename+ and return the exit code
|
925
1141
|
### from doing so.
|
926
1142
|
def edit( filename )
|
@@ -941,8 +1157,8 @@ class Treequel::Shell
|
|
941
1157
|
|
942
1158
|
### Return an ANSI-colored version of the given +rdn+ string.
|
943
1159
|
def format_rdn( rdn )
|
944
|
-
rdn.split( /,/ ).collect do |
|
945
|
-
key, val =
|
1160
|
+
rdn.split( /,/ ).collect do |rdn_part|
|
1161
|
+
key, val = rdn_part.split( /\s*=\s*/, 2 )
|
946
1162
|
colorize( :white ) { key } +
|
947
1163
|
colorize( :bold, :black ) { '=' } +
|
948
1164
|
colorize( :bold, :white ) { val }
|
@@ -952,15 +1168,33 @@ class Treequel::Shell
|
|
952
1168
|
|
953
1169
|
### Highlight LDIF and return it.
|
954
1170
|
def format_ldif( ldif )
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
1171
|
+
self.log.debug "Formatting LDIF: %p" % [ ldif ]
|
1172
|
+
return ldif.gsub( LDIF_ATTRVAL_SPEC ) do
|
1173
|
+
key, val = $1, $2.strip
|
1174
|
+
self.log.debug " formatting attribute: [ %p, %p ], remainder: %p" %
|
1175
|
+
[ key, val, $POSTMATCH ]
|
1176
|
+
|
1177
|
+
case val
|
1178
|
+
|
1179
|
+
# Base64-encoded value
|
1180
|
+
when /^:/
|
1181
|
+
val = val[1..-1].strip
|
1182
|
+
key +
|
1183
|
+
colorize( :dark, :green ) { ':: ' } +
|
1184
|
+
colorize( :green ) { val } + "\n"
|
1185
|
+
|
1186
|
+
# URL
|
1187
|
+
when /^</
|
1188
|
+
val = val[1..-1].strip
|
1189
|
+
key +
|
1190
|
+
colorize( :dark, :yellow ) { ':< ' } +
|
1191
|
+
colorize( :yellow ) { val } + "\n"
|
1192
|
+
|
1193
|
+
# Regular attribute
|
962
1194
|
else
|
963
|
-
key +
|
1195
|
+
key +
|
1196
|
+
colorize( :dark, :white ) { ': ' } +
|
1197
|
+
colorize( :bold, :white ) { val } + "\n"
|
964
1198
|
end
|
965
1199
|
end
|
966
1200
|
end
|
@@ -975,6 +1209,5 @@ class Treequel::Shell
|
|
975
1209
|
end # class Treequel::Shell
|
976
1210
|
|
977
1211
|
|
978
|
-
|
979
|
-
Treequel::Shell.new( ldapuri ).run
|
1212
|
+
Treequel::Shell.run( ARGV.dup )
|
980
1213
|
|