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.
Files changed (73) hide show
  1. data/ChangeLog +176 -14
  2. data/LICENSE +1 -1
  3. data/Rakefile +61 -45
  4. data/Rakefile.local +20 -0
  5. data/bin/treequel +502 -269
  6. data/examples/ldap-rack-auth.rb +2 -0
  7. data/lib/treequel.rb +221 -18
  8. data/lib/treequel/branch.rb +410 -201
  9. data/lib/treequel/branchcollection.rb +25 -13
  10. data/lib/treequel/branchset.rb +42 -40
  11. data/lib/treequel/constants.rb +233 -3
  12. data/lib/treequel/control.rb +95 -0
  13. data/lib/treequel/controls/contentsync.rb +138 -0
  14. data/lib/treequel/controls/pagedresults.rb +162 -0
  15. data/lib/treequel/controls/sortedresults.rb +216 -0
  16. data/lib/treequel/directory.rb +212 -65
  17. data/lib/treequel/exceptions.rb +11 -12
  18. data/lib/treequel/filter.rb +1 -12
  19. data/lib/treequel/mixins.rb +83 -47
  20. data/lib/treequel/monkeypatches.rb +29 -0
  21. data/lib/treequel/schema.rb +23 -19
  22. data/lib/treequel/schema/attributetype.rb +33 -3
  23. data/lib/treequel/schema/ldapsyntax.rb +0 -11
  24. data/lib/treequel/schema/matchingrule.rb +0 -11
  25. data/lib/treequel/schema/matchingruleuse.rb +0 -11
  26. data/lib/treequel/schema/objectclass.rb +36 -10
  27. data/lib/treequel/schema/table.rb +159 -0
  28. data/lib/treequel/sequel_integration.rb +7 -7
  29. data/lib/treequel/utils.rb +4 -66
  30. data/rake/documentation.rb +89 -0
  31. data/rake/helpers.rb +375 -307
  32. data/rake/hg.rb +16 -2
  33. data/rake/manual.rb +11 -6
  34. data/rake/packaging.rb +20 -35
  35. data/rake/publishing.rb +22 -62
  36. data/spec/lib/constants.rb +20 -0
  37. data/spec/lib/control_behavior.rb +44 -0
  38. data/spec/lib/matchers.rb +51 -0
  39. data/spec/treequel/branch_spec.rb +88 -29
  40. data/spec/treequel/branchcollection_spec.rb +24 -1
  41. data/spec/treequel/branchset_spec.rb +123 -51
  42. data/spec/treequel/control_spec.rb +48 -0
  43. data/spec/treequel/controls/contentsync_spec.rb +38 -0
  44. data/spec/treequel/controls/pagedresults_spec.rb +138 -0
  45. data/spec/treequel/controls/sortedresults_spec.rb +171 -0
  46. data/spec/treequel/directory_spec.rb +186 -16
  47. data/spec/treequel/mixins_spec.rb +42 -3
  48. data/spec/treequel/schema/attributetype_spec.rb +22 -20
  49. data/spec/treequel/schema/objectclass_spec.rb +67 -46
  50. data/spec/treequel/schema/table_spec.rb +134 -0
  51. data/spec/treequel_spec.rb +277 -15
  52. metadata +89 -108
  53. data/bin/treequel.orig +0 -963
  54. data/examples/ldap-monitor.rb +0 -143
  55. data/examples/ldap-monitor/public/css/master.css +0 -328
  56. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  57. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  58. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  59. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  60. data/examples/ldap-monitor/public/images/plug.png +0 -0
  61. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  62. data/examples/ldap-monitor/public/images/tick.png +0 -0
  63. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  64. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  65. data/examples/ldap-monitor/views/backends.erb +0 -41
  66. data/examples/ldap-monitor/views/connections.erb +0 -74
  67. data/examples/ldap-monitor/views/databases.erb +0 -39
  68. data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
  69. data/examples/ldap-monitor/views/index.erb +0 -14
  70. data/examples/ldap-monitor/views/layout.erb +0 -35
  71. data/examples/ldap-monitor/views/listeners.erb +0 -30
  72. data/rake/rdoc.rb +0 -30
  73. 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
- OPTION_PARSERS = {}
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
- OPTION_PARSERS[command.to_sym] = [oparser, options]
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 that will traverse the directory at the specified +uri+.
96
- def initialize( uri )
97
- Treequel.logger.level = Logger::WARN
98
- Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
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
- def run
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 + everything else
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 OPTION_PARSERS.key?( full_command )
168
- oparser, options = OPTION_PARSERS[ full_command ]
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.nitems, histfile ]
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.nitems > DEFAULT_HISTORY_SIZE
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.length == 1
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 OPTION_PARSERS.key?( cmd )
284
- oparser, _ = OPTION_PARSERS[ cmd ]
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
- @quit = true
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
- message( format_ldif(branch.to_ldif) )
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] [DNs]"
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
- rows << colorize( :underscore, :cyan ) { "total %d" % [children.length] }
569
+ header = colorize( :underscore, :cyan ) { "total %d" % [children.length] }
388
570
 
389
571
  # Calcuate column widths
390
- oclen = children.map do |branch|
391
- branch.include_operational_attrs = true
392
- branch[:structuralObjectClass].length
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
- children.
399
- sort_by {|branch| branch.rdn.downcase }.
400
- each do |branch|
401
- # -rw-r--r-- 2 mgranger staff 979 2009-07-27 11:55 Rakefile.local
402
- #
403
- # modifiersName: cn=admin,dc=laika,dc=com
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
- return rows
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
- return columnize( entries ).
426
- collect do |row|
427
- row.gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
428
- format_rdn( rdn )
429
- end
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
- #def edit_command( options, rdn )
466
- # branch = @currbranch.get_child( rdn )
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
- ### Convert the given +patterns+ to branchsets relative to the current branch and return
498
- ### them. This is used to map shell arguments like 'cn=*', 'Hosts', 'cn=dav*' into
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| " #{br.dn}" }.join("\n")
717
+ columnize( branches.collect {|br| br.dn } )
519
718
 
520
- ask_for_confirmation( msg ) do
521
- branches.each do |branch|
522
- branch.delete
523
- message "Deleted #{branch.dn}."
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 [DNs]"
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
- collect do |row|
543
- row.gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
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
- private
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
- message "Object as YAML is: ", yaml
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
- def branch_as_yaml( object, include_operational=false )
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
- dn = entryhash.delete( 'dn' )
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
- message( MULTILINE_PROMPT % [label] )
810
- if default
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
- results = []
815
- result = nil
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
- begin
818
- result = readline( make_prompt_string("> ") )
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
- results << result
1073
+ results << result
823
1074
  end
824
- end until result.nil? || result.empty?
1075
+ end until result.nil? || result.empty?
825
1076
 
826
- return results.flatten
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 |rdn|
945
- key, val = rdn.split( /\s*=\s*/, 2 )
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
- return ldif.gsub( /^(\S[^:]*)(::?)\s*(.*)$/ ) do
956
- key, sep, val = $1, $2, $3
957
- case sep
958
- when '::'
959
- colorize( :cyan ) { key } + ':: ' + colorize( :dark, :white ) { val }
960
- when ':'
961
- colorize( :bold, :cyan ) { key } + ': ' + colorize( :dark, :white ) { val }
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 + sep + ' ' + val
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
- ldapuri = URI( ARGV.shift || 'ldap://localhost' )
979
- Treequel::Shell.new( ldapuri ).run
1212
+ Treequel::Shell.run( ARGV.dup )
980
1213