treequel 1.0.1 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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