treequel 1.9.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
data.tar.gz.sig CHANGED
@@ -1 +1,2 @@
1
-
1
+ �G(�����WU�� B���V�V�'�2;��b�����M�*�O?��X����o����Ш�1,����1t�0L��Dx�3M���_�Y�܆�W���
2
+ �)�?��Ʈ �Ƣ�Q�ZK2)�B��_aBʒ�C�6��ԇ<>�w�Uv��ȉU�g;˞��7k蠥T{�D0g<.q0��������Ȭ�c]�s0j ���$-$Q��s�j��&��94�l�u�BBah�=�����E6@�O�x�c@�&#
data/ChangeLog CHANGED
@@ -1,3 +1,51 @@
1
+ 2012-08-29 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ * History.rdoc, lib/treequel.rb:
4
+ Bumped the minor version, updated history.
5
+ [0fa0df1acab0] [tip]
6
+
7
+ * .rvm.gems, Manifest.txt, README.rdoc, Rakefile, bin/treeirb,
8
+ bin/treequel, bin/treewhat, lib/treequel/branch.rb:
9
+ Split out the shell tools into their own gem
10
+ [9195b859cdc2]
11
+
12
+ 2012-06-07 Michael Granger <ged@FaerieMUD.org>
13
+
14
+ * .hgtags:
15
+ Added tag v1.9.1 for changeset f03ac5987586
16
+ [412e0978bd43]
17
+
18
+ * .hgsigs:
19
+ Added signature for changeset 2b31f8c2fe29
20
+ [f03ac5987586] [v1.9.1]
21
+
22
+ * History.rdoc, lib/treequel.rb:
23
+ Bump patch version, update history.
24
+ [2b31f8c2fe29]
25
+
26
+ * lib/treequel.rb, spec/treequel/monkeypatches_spec.rb:
27
+ Remove dependency on treequel/utils.
28
+ [a33cc598a3a7]
29
+
30
+ 2012-06-06 Michael Granger <ged@FaerieMUD.org>
31
+
32
+ * .hgtags:
33
+ Added tag v1.9.0 for changeset 83f7acbcac26
34
+ [1b3f2506002c]
35
+
36
+ * .hgsigs:
37
+ Added signature for changeset 29134510ef85
38
+ [83f7acbcac26] [v1.9.0]
39
+
40
+ * .rvm.gems, History.rdoc, Rakefile, lib/treequel.rb:
41
+ Bump the minor version, update History.
42
+ [29134510ef85]
43
+
44
+ * .rvm.gems, Manifest.txt, Rakefile, lib/treequel/utils.rb,
45
+ spec/lib/helpers.rb:
46
+ Update to Loggability 0.3.x.
47
+ [728b1d341366]
48
+
1
49
  2012-05-22 Michael Granger <ged@FaerieMUD.org>
2
50
 
3
51
  * .rvm.gems, .tm_properties, README.rdoc, lib/treequel.rb,
@@ -13,7 +61,7 @@
13
61
  spec/lib/helpers.rb, spec/treequel/directory_spec.rb,
14
62
  spec/treequel/mixins_spec.rb, spec/treequel_spec.rb:
15
63
  Convert to Loggability for logging
16
- [2e32ed84d899] [tip]
64
+ [2e32ed84d899]
17
65
 
18
66
  2012-04-16 Michael Granger <ged@FaerieMUD.org>
19
67
 
@@ -37,7 +85,7 @@
37
85
 
38
86
  * experiments/spec-profile-r408f5fadc4c5.graffle:
39
87
  Add spec profile
40
- [654ba9e47642] [github/master]
88
+ [654ba9e47642]
41
89
 
42
90
  * .hgignore, Manifest.txt, Rakefile, lib/treequel/constants.rb,
43
91
  lib/treequel/directory.rb, spec/lib/helpers.rb,
@@ -71,7 +119,7 @@
71
119
 
72
120
  Thanks to Mahlon E. Smith for pointing out the problem and pairing
73
121
  with me to fix it.
74
- [800c5704d3d0]
122
+ [800c5704d3d0] [github/master]
75
123
 
76
124
  * lib/treequel/mixins.rb, lib/treequel/model.rb:
77
125
  Clean up whitespace and fix grammar
@@ -110,6 +158,16 @@
110
158
  Whitespace cleanup.
111
159
  [de64de3dad50]
112
160
 
161
+ 2012-01-30 Michael Granger <ged@FaerieMUD.org>
162
+
163
+ * .hgtags:
164
+ Added tag v1.8.3 for changeset c9e91880eeb9
165
+ [142d9a553024]
166
+
167
+ * .hgsigs:
168
+ Added signature for changeset 5bab245f02aa
169
+ [c9e91880eeb9] [v1.8.3]
170
+
113
171
  2012-01-27 Michael Granger <ged@FaerieMUD.org>
114
172
 
115
173
  * .rspec-tm, .rvm.gems, .rvmrc:
@@ -122,16 +180,6 @@
122
180
  Fixing a link in the README
123
181
  [55b0f2e1048e]
124
182
 
125
- 2012-01-30 Michael Granger <ged@FaerieMUD.org>
126
-
127
- * .hgtags:
128
- Added tag v1.8.3 for changeset c9e91880eeb9
129
- [142d9a553024]
130
-
131
- * .hgsigs:
132
- Added signature for changeset 5bab245f02aa
133
- [c9e91880eeb9] [v1.8.3]
134
-
135
183
  2012-01-25 Michael Granger <ged@FaerieMUD.org>
136
184
 
137
185
  * lib/treequel/monkeypatches.rb:
data/History.rdoc CHANGED
@@ -1,3 +1,8 @@
1
+ == v1.10.0 [2012-08-29] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ - Split out the shell tools into their own gem
4
+
5
+
1
6
  == v1.9.1 [2012-06-07] Michael Granger <ged@FaerieMUD.org>
2
7
 
3
8
  - Fix dependency on eliminated file.
data/Manifest.txt CHANGED
@@ -5,9 +5,6 @@ LICENSE
5
5
  Manifest.txt
6
6
  README.rdoc
7
7
  Rakefile
8
- bin/treeirb
9
- bin/treequel
10
- bin/treewhat
11
8
  examples/company-directory.rb
12
9
  examples/ldap-rack-auth.rb
13
10
  examples/ldap_state.rb
data/README.rdoc CHANGED
@@ -16,6 +16,16 @@ It's inspired by and modeled after {Sequel}[http://sequel.rubyforge.org/], a
16
16
  kick-ass database library.
17
17
 
18
18
 
19
+ == Command-Line Tools
20
+
21
+ There are several command-line tools that are built on top of Treequel in
22
+ the 'treequel-shell' gem. They include:
23
+
24
+ treequel :: an LDAP shell/editor; treat your LDAP directory like a filesystem!
25
+ treewhat :: an LDAP schema explorer. Dump objectClasses and attribute type info
26
+ in several convenient formats.
27
+
28
+
19
29
  == Contributing
20
30
 
21
31
  You can check out the current development source
data/Rakefile CHANGED
@@ -35,24 +35,15 @@ hoespec = Hoe.spec 'treequel' do
35
35
  self.dependency 'loggability', '~> 0.4'
36
36
 
37
37
  self.dependency 'rspec', '~> 2.8', :developer
38
- self.dependency 'ruby-termios', '~> 0.9', :developer
39
- self.dependency 'ruby-terminfo', '~> 0.1', :developer
40
- self.dependency 'columnize', '~> 0.3', :developer
41
- self.dependency 'sysexits', '~> 1.0', :developer
42
- self.dependency 'sequel', '~> 3.20', :developer
38
+ self.dependency 'sequel', '~> 3.38', :developer
43
39
 
44
40
  self.spec_extras[:licenses] = ["BSD"]
45
41
  self.spec_extras[:post_install_message] = [
46
- "If you want to use the included 'treequel' LDAP shell, you'll need to install",
47
- "the following libraries as well:",
48
- '',
49
- " - ruby-termios",
50
- " - ruby-terminfo",
51
- " - columnize",
52
- " - sysexits",
53
- '',
54
- "You can install them automatically if you use the --development flag when",
55
- "installing Treequel."
42
+ '-' * 72,
43
+ "NOTE: The Treequel command-line tools are no longer distributed ",
44
+ "with the Treequel gem; to get the tools, install the 'treequel-shell' ",
45
+ "gem. Thanks!",
46
+ '-' * 72
56
47
  ].join( "\n" )
57
48
 
58
49
  self.require_ruby_version( '>=1.8.7' )
@@ -69,16 +60,6 @@ ENV['VERSION'] ||= hoespec.spec.version.to_s
69
60
  # Ensure the specs pass before checking in
70
61
  task 'hg:precheckin' => [ 'ChangeLog', :check_history, :check_manifest, :spec ]
71
62
 
72
- ### Make the ChangeLog update if the repo has changed since it was last built
73
- file '.hg/branch'
74
- file 'ChangeLog' => '.hg/branch' do |task|
75
- $stderr.puts "Updating the changelog..."
76
- content = make_changelog()
77
- File.open( task.name, 'w', 0644 ) do |fh|
78
- fh.print( content )
79
- end
80
- end
81
-
82
63
  # Rebuild the ChangeLog immediately before release
83
64
  task :prerelease => 'ChangeLog'
84
65
 
data/lib/treequel.rb CHANGED
@@ -32,10 +32,10 @@ module Treequel
32
32
 
33
33
 
34
34
  # Library version
35
- VERSION = '1.9.1'
35
+ VERSION = '1.10.0'
36
36
 
37
37
  # VCS revision
38
- REVISION = %q$Revision: 2b31f8c2fe29 $
38
+ REVISION = %q$Revision: 0fa0df1acab0 $
39
39
 
40
40
  # Common paths for ldap.conf
41
41
  COMMON_LDAP_CONF_PATHS = %w[
@@ -72,8 +72,8 @@ class Treequel::Branch
72
72
  ### I N S T A N C E M E T H O D S
73
73
  #################################################################
74
74
 
75
- ### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+.
76
- ### If the optional +entry+ object is given, it will be used to fetch values from the
75
+ ### Create a new Treequel::Branch with the given +directory+, +dn+, and an optional +entry+.
76
+ ### If the optional +entry+ object is given, it will be used to fetch values from the
77
77
  ### directory; if it isn't provided, it will be fetched from the +directory+ the first
78
78
  ### time it is needed.
79
79
  def initialize( directory, dn, entry=nil )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -37,7 +37,7 @@ cert_chain:
37
37
  YUhDS0xaZFNLai9SSHVUT3QrZ2JsUmV4OEZBaDhOZUEKY21saFhlNDZwWk5K
38
38
  Z1dLYnhaYWg4NWpJang5NWhSOHZPSStOQU01aUg5a09xSzEzRHJ4YWNUS1Bo
39
39
  cWo1UGp3RgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
40
- date: 2012-06-07 00:00:00.000000000 Z
40
+ date: 2012-08-29 00:00:00.000000000 Z
41
41
  dependencies:
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: ruby-ldap
@@ -135,70 +135,6 @@ dependencies:
135
135
  - - ~>
136
136
  - !ruby/object:Gem::Version
137
137
  version: '2.8'
138
- - !ruby/object:Gem::Dependency
139
- name: ruby-termios
140
- requirement: !ruby/object:Gem::Requirement
141
- none: false
142
- requirements:
143
- - - ~>
144
- - !ruby/object:Gem::Version
145
- version: '0.9'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- none: false
150
- requirements:
151
- - - ~>
152
- - !ruby/object:Gem::Version
153
- version: '0.9'
154
- - !ruby/object:Gem::Dependency
155
- name: ruby-terminfo
156
- requirement: !ruby/object:Gem::Requirement
157
- none: false
158
- requirements:
159
- - - ~>
160
- - !ruby/object:Gem::Version
161
- version: '0.1'
162
- type: :development
163
- prerelease: false
164
- version_requirements: !ruby/object:Gem::Requirement
165
- none: false
166
- requirements:
167
- - - ~>
168
- - !ruby/object:Gem::Version
169
- version: '0.1'
170
- - !ruby/object:Gem::Dependency
171
- name: columnize
172
- requirement: !ruby/object:Gem::Requirement
173
- none: false
174
- requirements:
175
- - - ~>
176
- - !ruby/object:Gem::Version
177
- version: '0.3'
178
- type: :development
179
- prerelease: false
180
- version_requirements: !ruby/object:Gem::Requirement
181
- none: false
182
- requirements:
183
- - - ~>
184
- - !ruby/object:Gem::Version
185
- version: '0.3'
186
- - !ruby/object:Gem::Dependency
187
- name: sysexits
188
- requirement: !ruby/object:Gem::Requirement
189
- none: false
190
- requirements:
191
- - - ~>
192
- - !ruby/object:Gem::Version
193
- version: '1.0'
194
- type: :development
195
- prerelease: false
196
- version_requirements: !ruby/object:Gem::Requirement
197
- none: false
198
- requirements:
199
- - - ~>
200
- - !ruby/object:Gem::Version
201
- version: '1.0'
202
138
  - !ruby/object:Gem::Dependency
203
139
  name: sequel
204
140
  requirement: !ruby/object:Gem::Requirement
@@ -206,7 +142,7 @@ dependencies:
206
142
  requirements:
207
143
  - - ~>
208
144
  - !ruby/object:Gem::Version
209
- version: '3.20'
145
+ version: '3.38'
210
146
  type: :development
211
147
  prerelease: false
212
148
  version_requirements: !ruby/object:Gem::Requirement
@@ -214,7 +150,7 @@ dependencies:
214
150
  requirements:
215
151
  - - ~>
216
152
  - !ruby/object:Gem::Version
217
- version: '3.20'
153
+ version: '3.38'
218
154
  - !ruby/object:Gem::Dependency
219
155
  name: hoe
220
156
  requirement: !ruby/object:Gem::Requirement
@@ -245,10 +181,7 @@ description: ! 'Treequel is an LDAP toolkit for Ruby. It is intended to allow qu
245
181
  email:
246
182
  - ged@FaerieMUD.org
247
183
  - mahlon@martini.nu
248
- executables:
249
- - treeirb
250
- - treequel
251
- - treewhat
184
+ executables: []
252
185
  extensions: []
253
186
  extra_rdoc_files:
254
187
  - History.rdoc
@@ -262,9 +195,6 @@ files:
262
195
  - Manifest.txt
263
196
  - README.rdoc
264
197
  - Rakefile
265
- - bin/treeirb
266
- - bin/treequel
267
- - bin/treewhat
268
198
  - examples/company-directory.rb
269
199
  - examples/ldap-rack-auth.rb
270
200
  - examples/ldap_state.rb
@@ -330,10 +260,9 @@ files:
330
260
  homepage: http://deveiate.org/projects/Treequel
331
261
  licenses:
332
262
  - BSD
333
- post_install_message: ! "If you want to use the included 'treequel' LDAP shell, you'll
334
- need to install\nthe following libraries as well:\n\n - ruby-termios\n - ruby-terminfo\n
335
- \ - columnize\n - sysexits\n\nYou can install them automatically if you use
336
- the --development flag when\ninstalling Treequel."
263
+ post_install_message: ! "------------------------------------------------------------------------\nNOTE:
264
+ The Treequel command-line tools are no longer distributed \nwith the Treequel gem;
265
+ to get the tools, install the 'treequel-shell' \ngem. Thanks!\n------------------------------------------------------------------------"
337
266
  rdoc_options:
338
267
  - -f
339
268
  - fivefish
metadata.gz.sig CHANGED
Binary file
data/bin/treeirb DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'irb'
4
- require 'irb/extend-command'
5
- require 'irb/cmd/nop'
6
- require 'treequel'
7
-
8
-
9
- if uri = ARGV.shift
10
- $dir = Treequel.directory( uri )
11
- else
12
- $dir = Treequel.directory_from_config
13
- end
14
-
15
- $stderr.puts "Directory is in $dir:", ' ' + $dir.inspect
16
-
17
- IRB.start( $0 )
18
-
data/bin/treequel DELETED
@@ -1,1276 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'abbrev'
4
- require 'columnize'
5
- require 'diff/lcs'
6
- require 'digest/sha1'
7
- require 'irb'
8
- require 'logger'
9
- require 'open3'
10
- require 'optparse'
11
- require 'ostruct'
12
- require 'pathname'
13
- require 'readline'
14
- require 'shellwords'
15
- require 'tempfile'
16
- require 'terminfo'
17
- require 'termios'
18
- require 'uri'
19
- require 'yaml'
20
-
21
- require 'treequel'
22
- require 'treequel/mixins'
23
- require 'treequel/constants'
24
-
25
-
26
- ### Monkeypatch for resetting an OpenStruct's state.
27
- class OpenStruct
28
-
29
- ### Clear all defined fields and values.
30
- def clear
31
- @table.clear
32
- end
33
-
34
- end
35
-
36
-
37
- ### IRb.start_session, courtesy of Joel VanderWerf in [ruby-talk:42437].
38
- require 'irb'
39
- require 'irb/completion'
40
-
41
- module IRB # :nodoc:
42
- def self.start_session( obj )
43
- unless @__initialized
44
- args = ARGV
45
- ARGV.replace( [] )
46
- IRB.setup( nil )
47
- ARGV.replace( args )
48
- @__initialized = true
49
- end
50
-
51
- workspace = WorkSpace.new( obj )
52
- irb = Irb.new( workspace )
53
-
54
- @CONF[:IRB_RC].call( irb.context ) if @CONF[:IRB_RC]
55
- @CONF[:MAIN_CONTEXT] = irb.context
56
-
57
- begin
58
- prevhandler = Signal.trap( 'INT' ) do
59
- irb.signal_handle
60
- end
61
-
62
- catch( :IRB_EXIT ) do
63
- irb.eval_input
64
- end
65
- ensure
66
- Signal.trap( 'INT', prevhandler )
67
- end
68
-
69
- end
70
- end
71
-
72
- # The Treequel shell.
73
- #
74
- # TODO:
75
- # * Make more commands use the convert_to_branchsets utility function
76
- #
77
- class Treequel::Shell
78
- include Readline,
79
- Treequel::Loggable,
80
- Treequel::ANSIColorUtilities,
81
- Treequel::Constants::Patterns,
82
- Treequel::HashUtilities
83
-
84
- extend Treequel::ANSIColorUtilities
85
-
86
- # Prompt text for #prompt_for_multiple_values
87
- MULTILINE_PROMPT = <<-'EOF'
88
- Enter one or more values for '%s'.
89
- A blank line finishes input.
90
- EOF
91
-
92
- # Some ANSI codes for fancier stuff
93
- CLEAR_TO_EOL = "\e[K"
94
- CLEAR_CURRENT_LINE = "\e[2K"
95
-
96
- # Valid connect-type arguments
97
- VALID_CONNECT_TYPES = %w[tls ssl plain]
98
-
99
- # Command option parsers
100
- @@option_parsers = {}
101
-
102
- # Path to the default history file
103
- HISTORY_FILE = Pathname( "~/.treequel.history" )
104
-
105
- # Number of items to store in history by default
106
- DEFAULT_HISTORY_SIZE = 100
107
-
108
- # The default editor, in case ENV['VISUAL'] and ENV['EDITOR'] are unset
109
- DEFAULT_EDITOR = 'vi'
110
-
111
-
112
- #################################################################
113
- ### C L A S S M E T H O D S
114
- #################################################################
115
-
116
- ### Run the shell.
117
- def self::run( args )
118
- Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
119
- bind_as, plaintext, uri = self.parse_options( args )
120
-
121
- connect_type = plaintext ? :plain : :tls
122
-
123
- directory = if uri
124
- Treequel.directory( uri, :connect_type => connect_type )
125
- else
126
- Treequel.directory_from_config
127
- end
128
-
129
- Treequel::Shell.new( directory ).run( bind_as )
130
- end
131
-
132
-
133
- ### Parse command-line options for shell startup and return an options struct and
134
- ### the LDAP URI.
135
- def self::parse_options( argv )
136
- progname = File.basename( $0 )
137
- loglevels = Treequel::LOG_LEVELS.
138
- sort_by {|_,lvl| lvl }.
139
- collect {|name,lvl| name.to_s }.
140
- join(', ')
141
- bind_as = nil
142
- plaintext = false
143
-
144
- oparser = OptionParser.new( "Usage: #{progname} [OPTIONS] [LDAPURL]" ) do |oparser|
145
- oparser.separator ' '
146
-
147
- oparser.on( '--binddn=DN', '-b DN', String, "Bind as DN" ) do |dn|
148
- bind_as = dn
149
- end
150
-
151
- oparser.on( '--no-tls', FalseClass, "Use a plaintext (unencrypted) connection.",
152
- "If you don't specify a connection URL, this option is ignored." ) do
153
- plaintext = true
154
- end
155
-
156
- oparser.on( '--loglevel=LEVEL', '-l LEVEL', Treequel::LOG_LEVELS.keys,
157
- "Set the logging level. Should be one of:", loglevels ) do |lvl|
158
- Treequel.logger.level = Treequel::LOG_LEVELS[ lvl ] or
159
- raise "Invalid logging level %p" % [ lvl ]
160
- end
161
-
162
- oparser.on( '--debug', '-d', FalseClass, "Turn debugging on" ) do
163
- $DEBUG = true
164
- $trace = true
165
- Treequel.logger.level = Logger::DEBUG
166
- end
167
-
168
- oparser.on("-h", "--help", "Show this help message.") do
169
- $stderr.puts( oparser )
170
- exit!
171
- end
172
- end
173
-
174
- remaining_args = oparser.parse( argv )
175
-
176
- return bind_as, plaintext, *remaining_args
177
- end
178
-
179
-
180
- ### Create an option parser from the specified +block+ for the given +command+ and register
181
- ### it. Many thanks to apeiros and dominikh on #Ruby-Pro for the ideas behind this.
182
- def self::set_options( command, &block )
183
- options = OpenStruct.new
184
- oparser = OptionParser.new( "Help for #{command}" ) do |o|
185
- yield( o, options )
186
- end
187
- oparser.default_argv = []
188
-
189
- @@option_parsers[command.to_sym] = [oparser, options]
190
- end
191
-
192
-
193
- #################################################################
194
- ### I N S T A N C E M E T H O D S
195
- #################################################################
196
-
197
- ### Create a new shell for the specified +directory+ (a Treequel::Directory).
198
- def initialize( directory )
199
- @dir = directory
200
- @uri = directory.uri
201
- @quit = false
202
- @currbranch = @dir
203
- @columns = TermInfo.screen_width
204
- @rows = TermInfo.screen_height
205
-
206
- @commands = self.find_commands
207
- @completions = @commands.abbrev
208
- @command_table = make_command_table( @commands )
209
- end
210
-
211
-
212
- ######
213
- public
214
- ######
215
-
216
- # The number of columns in the current terminal
217
- attr_reader :columns
218
-
219
- # The number of rows in the current terminal
220
- attr_reader :rows
221
-
222
- # The flag which causes the shell to exit after the current loop
223
- attr_accessor :quit
224
-
225
-
226
- ### The command loop: run the shell until the user wants to quit, binding as +bind_as+ if
227
- ### given.
228
- def run( bind_as=nil )
229
- @original_tty_settings = IO.read( '|-' ) or exec 'stty', '-g'
230
- message "Connected to %s" % [ @uri ]
231
-
232
- # Set up the completion callback
233
- self.setup_completion
234
-
235
- # Load saved command-line history
236
- self.read_history
237
-
238
- # If the user said to bind as someone on the command line, invoke a
239
- # 'bind' command before dropping into the command line
240
- if bind_as
241
- options = OpenStruct.new # dummy options object
242
- self.bind_command( options, bind_as )
243
- end
244
-
245
- # Run until something sets the quit flag
246
- until @quit
247
- $stderr.puts
248
- prompt = make_prompt_string( @currbranch.dn + '> ' )
249
- input = Readline.readline( prompt, true )
250
- self.log.debug "Input is: %p" % [ input ]
251
-
252
- # EOL makes the shell quit
253
- if input.nil?
254
- self.log.debug "EOL: setting quit flag"
255
- @quit = true
256
-
257
- # Blank input -- just reprompt
258
- elsif input == ''
259
- self.log.debug "No command. Re-displaying the prompt."
260
-
261
- # Parse everything else into command + args
262
- else
263
- self.log.debug "Dispatching input: %p" % [ input ]
264
- self.dispatch_cmd( input )
265
- end
266
- end
267
-
268
- message "\nSaving history...\n"
269
- self.save_history
270
-
271
- message "done."
272
-
273
- rescue => err
274
- error_message( err.class.name, err.message )
275
- err.backtrace.each do |frame|
276
- self.log.debug " " + frame
277
- end
278
-
279
- ensure
280
- system( 'stty', @original_tty_settings.chomp )
281
- end
282
-
283
-
284
- ### Parse the specified +input+ into a command, options, and arguments and dispatch them
285
- ### to the appropriate command method.
286
- def dispatch_cmd( input )
287
- command, *args = Shellwords.shellwords( input )
288
-
289
- # If it's a valid command, run it
290
- if meth = @command_table[ command ]
291
- full_command = @completions[ command ].to_sym
292
-
293
- # If there's a registered optionparser for the command, use it to
294
- # split out options and arguments, then pass those to the command.
295
- if @@option_parsers.key?( full_command )
296
- oparser, options = @@option_parsers[ full_command ]
297
- self.log.debug "Got an option-parser for #{full_command}."
298
-
299
- cmdargs = oparser.parse( args )
300
- self.log.debug " options=%p, args=%p" % [ options, cmdargs ]
301
- meth.call( options, *cmdargs )
302
-
303
- options.clear
304
-
305
- # ...otherwise just call it with all the args.
306
- else
307
- self.log.warn " no options defined for '%s' command" % [ command ]
308
- meth.call( *args )
309
- end
310
-
311
- # ...otherwise call the fallback handler
312
- else
313
- self.handle_missing_cmd( command )
314
- end
315
-
316
- rescue LDAP::ResultError => err
317
- case err.message
318
- when /can't contact ldap server/i
319
- if @dir.connected?
320
- error_message( "LDAP connection went away." )
321
- else
322
- error_message( "Couldn't connect to the server." )
323
- end
324
- ask_for_confirmation( "Attempt to reconnect?" ) do
325
- @dir.reconnect
326
- end
327
- retry
328
-
329
- when /invalid credentials/i
330
- error_message( "Authentication failed." )
331
- else
332
- error_message( err.class.name, err.message )
333
- self.log.debug { " " + err.backtrace.join(" \n") }
334
- end
335
-
336
- rescue => err
337
- error_message( err.message )
338
- self.log.debug { " " + err.backtrace.join(" \n") }
339
- end
340
-
341
-
342
-
343
- #########
344
- protected
345
- #########
346
-
347
- ### Set up Readline completion
348
- def setup_completion
349
- Readline.completion_proc = self.method( :completion_callback ).to_proc
350
- Readline.completer_word_break_characters = ''
351
- Readline.basic_word_break_characters = ''
352
- end
353
-
354
-
355
- ### Read command line history from HISTORY_FILE
356
- def read_history
357
- histfile = HISTORY_FILE.expand_path
358
-
359
- if histfile.exist?
360
- lines = histfile.readlines.collect {|line| line.chomp }
361
- self.log.debug "Read %d saved history commands from %s." % [ lines.length, histfile ]
362
- Readline::HISTORY.push( *lines )
363
- else
364
- self.log.debug "History file '%s' was empty or non-existant." % [ histfile ]
365
- end
366
- end
367
-
368
-
369
- ### Save command line history to HISTORY_FILE
370
- def save_history
371
- histfile = HISTORY_FILE.expand_path
372
-
373
- lines = Readline::HISTORY.to_a.reverse.uniq.reverse
374
- lines = lines[ -DEFAULT_HISTORY_SIZE, DEFAULT_HISTORY_SIZE ] if
375
- lines.length > DEFAULT_HISTORY_SIZE
376
-
377
- self.log.debug "Saving %d history lines to %s." % [ lines.length, histfile ]
378
-
379
- histfile.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
380
- ofh.puts( *lines )
381
- end
382
- end
383
-
384
-
385
- ### Handle completion requests from Readline.
386
- def completion_callback( input )
387
- self.log.debug "Input completion: %p" % [ input ]
388
- parts = Shellwords.shellwords( input )
389
-
390
- # If there aren't any arguments, it's command completion
391
- if parts.empty?
392
- possible_completions = @commands.sort
393
- self.log.debug " possible completions: %p" % [ possible_completions ]
394
- return possible_completions
395
- elsif parts.length == 1
396
- # One completion means it's an unambiguous match, so just complete it.
397
- possible_completions = @commands.grep( /^#{Regexp.quote(input)}/ ).sort
398
- self.log.debug " possible completions: %p" % [ possible_completions ]
399
- return possible_completions
400
- else
401
- incomplete = parts.pop
402
- self.log.debug " the incomplete bit is: %p" % [ incomplete ]
403
- possible_completions = @currbranch.children.
404
- collect {|br| br.rdn }.grep( /^#{Regexp.quote(incomplete)}/i ).sort
405
-
406
- possible_completions.map! do |lastpart|
407
- parts.join( ' ' ) + ' ' + lastpart
408
- end
409
-
410
- self.log.debug " possible (argument) completions: %p" % [ possible_completions ]
411
- return possible_completions
412
- end
413
- end
414
-
415
-
416
- #################################################################
417
- ### C O M M A N D S
418
- #################################################################
419
-
420
- ### Show the completions hash
421
- def show_completions_command
422
- message "Completions:", @completions.inspect
423
- end
424
- set_options :show_completions do |oparser, options|
425
- oparser.banner = "show_completions"
426
- oparser.separator 'Show the list of command completions (for debugging the shell)'
427
- end
428
-
429
-
430
- ### Show help text for the specified command, or a list of all available commands
431
- ### if none is specified.
432
- def help_command( options, *args )
433
- if args.empty?
434
- $stderr.puts
435
- message colorize( "Available commands", :bold, :white ),
436
- *columnize(@commands)
437
- else
438
- cmd = args.shift
439
- full_command = @completions[ cmd ]
440
-
441
- if @@option_parsers.key?( full_command.to_sym )
442
- oparser, _ = @@option_parsers[ full_command.to_sym ]
443
- self.log.debug "Setting summary width to: %p" % [ @columns ]
444
- oparser.summary_width = @columns
445
- output = oparser.to_s.sub( /^(.*?)\n/ ) do |match|
446
- colorize( :bold, :white ) { match }
447
- end
448
-
449
- $stderr.puts
450
- message( output )
451
- else
452
- error_message( "No help for '#{cmd}'" )
453
- end
454
- end
455
- end
456
- set_options :help do |oparser, options|
457
- oparser.banner = "help [COMMAND]"
458
- oparser.separator 'Display general help, or help for a specific COMMAND.'
459
- end
460
-
461
-
462
- ### Quit the shell.
463
- def quit_command( options, *args )
464
- message "Okay, exiting."
465
- self.quit = true
466
- end
467
- set_options :quit do |oparser, options|
468
- oparser.banner = "quit"
469
- oparser.separator 'Exit the shell.'
470
- end
471
-
472
-
473
- ### Set the logging level (if invoked with an argument) or display the current
474
- ### level (with no argument).
475
- def log_command( options, *args )
476
- newlevel = args.shift
477
- if newlevel
478
- if Treequel::LOG_LEVELS.key?( newlevel )
479
- Treequel.logger.level = Treequel::LOG_LEVELS[ newlevel ]
480
- message "Set log level to: %s" % [ newlevel ]
481
- else
482
- levelnames = Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
483
- raise "Invalid log level %p: valid values are:\n %s" % [ newlevel, levelnames ]
484
- end
485
- else
486
- message "Log level is currently: %s" %
487
- [ Treequel::LOG_LEVEL_NAMES[Treequel.logger.level] ]
488
- end
489
- end
490
- set_options :log do |oparser, options|
491
- oparser.banner = "log [LEVEL]"
492
- oparser.separator 'Set the logging level, or display the current level if no level ' +
493
- "is given. Valid log levels are: %s" %
494
- Treequel::LOG_LEVEL_NAMES.keys.sort.join(', ')
495
- end
496
-
497
-
498
- ### Display LDIF for the specified RDNs.
499
- def cat_command( options, *args )
500
- validate_rdns( *args )
501
- args.each do |rdn|
502
- extended = rdn.chomp!( '+' )
503
-
504
- branch = @currbranch.get_child( rdn )
505
- branch.include_operational_attrs = true if extended
506
-
507
- if branch.exists?
508
- ldifstring = branch.to_ldif( self.columns - 2 )
509
- self.log.debug "LDIF: #{ldifstring.dump}"
510
-
511
- message( format_ldif(ldifstring) )
512
- else
513
- error_message( "No such entry %s" % [branch.dn] )
514
- end
515
- end
516
- end
517
- set_options :cat do |oparser, options|
518
- oparser.banner = "cat [RDN]+"
519
- oparser.separator 'Display the entries specified by RDN as LDIF.'
520
- end
521
-
522
-
523
- ### Display YAML for the specified RDNs.
524
- def yaml_command( options, *args )
525
- validate_rdns( *args )
526
- args.each do |rdn|
527
- branch = @currbranch.get_child( rdn )
528
- message( branch_as_yaml(branch) )
529
- end
530
- end
531
- set_options :yaml do |oparser, options|
532
- oparser.banner = "yaml [RDN]+"
533
- oparser.separator 'Display the entries specified by RDN as YAML.'
534
- end
535
-
536
-
537
- ### List the children of the branch specified by the given +rdn+, or the current branch if none
538
- ### are specified.
539
- def ls_command( options, *args )
540
- targets = []
541
-
542
- # No argument, just use the current branch
543
- if args.empty?
544
- targets << @currbranch
545
-
546
- # Otherwise, list each one specified
547
- else
548
- validate_rdns( *args )
549
- args.each do |rdn|
550
- if branch = @currbranch.get_child( rdn )
551
- targets << branch
552
- else
553
- error_message( "cannot access #{rdn}: no such entry" )
554
- end
555
- end
556
- end
557
-
558
- # Fetch each branch's children, sort them, format them in columns, and highlight them
559
- targets.each do |branch|
560
- header( branch.dn ) if targets.length > 1
561
- if options.longform
562
- message self.make_longform_ls_output( branch, options )
563
- else
564
- message self.make_shortform_ls_output( branch, options )
565
- end
566
- message if targets.length > 1
567
- end
568
- end
569
- set_options :ls do |oparser, options|
570
- oparser.banner = "ls [OPTIONS] [DN]+"
571
- oparser.separator 'List the entries specified, or the current entry if none are specified.'
572
- oparser.separator ''
573
-
574
- oparser.on( "-l", "--long", FalseClass, "List in long format." ) do
575
- options.longform = true
576
- end
577
- oparser.on( "-t", "--timesort", FalseClass,
578
- "Sort by time modified (most recently modified first)." ) do
579
- options.timesort = true
580
- end
581
- oparser.on( "-d", "--dirsort", FalseClass,
582
- "Sort entries with subordinate entries before those without." ) do
583
- options.dirsort = true
584
- end
585
- oparser.on( "-r", "--reverse", FalseClass, "Reverse the entry sort functions." ) do
586
- options.reversesort = true
587
- end
588
-
589
- end
590
-
591
-
592
- ### Change the current working DN to +rdn+.
593
- def cdn_command( options, rdn=nil, *args )
594
- if rdn.nil?
595
- @currbranch = @dir.base
596
- return
597
- end
598
-
599
- return self.parent_command( options ) if rdn == '..'
600
-
601
- validate_rdns( rdn )
602
-
603
- pairs = rdn.split( /\s*,\s*/ )
604
- pairs.each do |dnpair|
605
- self.log.debug " cd to %p" % [ dnpair ]
606
- attribute, value = dnpair.split( /=/, 2 )
607
- self.log.debug " changing to %s( %p )" % [ attribute.downcase, value ]
608
- @currbranch = @currbranch.send( attribute.downcase, value )
609
- end
610
- end
611
- set_options :cdn do |oparser, options|
612
- oparser.banner = "cdn <RDN>"
613
- oparser.separator 'Change the current entry to <RDN>.'
614
- end
615
-
616
-
617
- ### Change the current working DN to the current entry's parent.
618
- def parent_command( options, *args )
619
- parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]
620
-
621
- self.log.debug " changing to %s" % [ parent.dn ]
622
- @currbranch = parent
623
- end
624
- set_options :parent do |oparser, options|
625
- oparser.banner = "parent"
626
- oparser.separator "Change to the current entry's parent."
627
- end
628
-
629
-
630
- # ### Create the entry specified by +rdn+.
631
- def create_command( options, rdn )
632
- validate_rdns( rdn )
633
- branch = @currbranch.get_child( rdn )
634
-
635
- raise "#{branch.dn}: already exists." if branch.exists?
636
- create_new_entry( branch )
637
- end
638
- set_options :create do |oparser, options|
639
- oparser.banner = "create <RDN>"
640
- oparser.separator "Create a new entry at <RDN>."
641
- end
642
-
643
-
644
- ### Edit the entry specified by +rdn+.
645
- def edit_command( options, rdn )
646
- validate_rdns( rdn )
647
- branch = @currbranch.get_child( rdn )
648
-
649
- raise "#{branch.dn}: no such entry. Did you mean to 'create' it instead? " unless
650
- branch.exists?
651
-
652
- if entryhash = edit_in_yaml( branch )
653
- branch.merge( entryhash )
654
- end
655
-
656
- message "Saved #{rdn}."
657
- end
658
- set_options :edit do |oparser, options|
659
- oparser.banner = "edit <RDN>"
660
- oparser.separator "Edit the entry at RDN as YAML."
661
- end
662
-
663
-
664
- ### Change the DN of an entry
665
- def mv_command( options, rdn, newdn )
666
- validate_rdns( rdn, newdn )
667
- branch = @currbranch.get_child( rdn )
668
-
669
- raise "#{branch.dn}: no such entry" unless branch.exists?
670
- olddn = branch.dn
671
- branch.move( newdn )
672
- message " %s -> %s: success" % [ olddn, branch.dn ]
673
- end
674
- set_options :mv do |oparser, options|
675
- oparser.banner = "mv <RDN> <NEWRDN>"
676
- oparser.separator "Move the entry at RDN to NEWRDN"
677
- end
678
-
679
-
680
- ### Copy an entry
681
- def cp_command( options, rdn, newrdn )
682
- # Can't validate as RDNs because they might be full DNs
683
-
684
- base_dn = @currbranch.directory.base_dn
685
-
686
- # If the RDN includes the base, it's a DN
687
- branch = if rdn =~ /,#{base_dn}$/i
688
- Treequel::Branch.new( @currbranch.directory, rdn )
689
- else
690
- @currbranch.get_child( rdn )
691
- end
692
-
693
- # The source should already exist
694
- raise "#{branch.dn}: no such entry" unless branch.exists?
695
-
696
- # Same for the other RDN...
697
- newbranch = if newrdn =~ /,#{base_dn}$/i
698
- Treequel::Branch.new( @currbranch.directory, newrdn )
699
- else
700
- @currbranch.get_child( newrdn )
701
- end
702
-
703
- # But it *shouldn't* exist already
704
- raise "#{newbranch.dn}: already exists" if newbranch.exists?
705
-
706
- attributes = branch.entry.merge( :dn => newbranch.dn )
707
- newbranch.create( attributes )
708
-
709
- message " %s -> %s: success" % [ rdn, branch.dn ]
710
- end
711
- set_options :cp do |oparser, options|
712
- oparser.banner = "cp <RDN> <NEWRDN>"
713
- oparser.separator "Copy the entry at RDN to a new entry at NEWRDN"
714
- end
715
-
716
-
717
- ### Remove the entry specified by +rdn+.
718
- def rm_command( options, *rdns )
719
- validate_rdns( *rdns )
720
- branchsets = self.convert_to_branchsets( *rdns )
721
- coll = Treequel::BranchCollection.new( *branchsets )
722
-
723
- branches = coll.all
724
-
725
- msg = "About to delete the following entries:\n" +
726
- columnize( branches.collect {|br| br.dn } )
727
-
728
- if options.force
729
- branches.each do |br|
730
- br.directory.delete( br )
731
- message " delete %s: success" % [ br.dn ]
732
- end
733
- else
734
- ask_for_confirmation( msg ) do
735
- branches.each do |br|
736
- br.directory.delete( br )
737
- message " delete %s: success" % [ br.dn ]
738
- end
739
- end
740
- end
741
- end
742
- set_options :rm do |oparser, options|
743
- oparser.banner = "rm <RDN>+"
744
- oparser.separator 'Remove the entries at the given RDNs.'
745
-
746
- oparser.on( '-f', '--force', TrueClass, "Force -- remove without confirmation." ) do
747
- options.force = true
748
- end
749
- end
750
-
751
-
752
- ### Find entries that match the given filter_clauses.
753
- def grep_command( options, *filter_clauses )
754
- branchset = filter_clauses.inject( @currbranch ) do |branch, clause|
755
- branch.filter( clause )
756
- end
757
-
758
- message "Searching for entries that match '#{branchset.to_s}'"
759
-
760
- entries = branchset.all
761
- output = columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
762
- format_rdn( rdn )
763
- end
764
- message( output )
765
- end
766
- set_options :grep do |oparser, options|
767
- oparser.banner = "grep [OPTIONS] <FILTER>"
768
- oparser.separator 'Search for children of the current entry that match the given FILTER'
769
-
770
- oparser.on( '-r', '--recursive', TrueClass, "Search recursively." ) do
771
- options.force = true
772
- end
773
- end
774
-
775
-
776
- ### Show who the shell is currently bound as.
777
- def whoami_command( options, *args )
778
- if user = @dir.bound_user
779
- message "Bound as #{user}"
780
- else
781
- message "Bound anonymously"
782
- end
783
- end
784
- set_options :whoami do |oparser, options|
785
- oparser.banner = "whoami"
786
- oparser.separator 'Display the DN of the user the shell is bound as.'
787
- end
788
-
789
-
790
- ### Bind as a user.
791
- def bind_command( options, *args )
792
- binddn = (args.first || prompt( "Bind DN/UID" )) or
793
- raise "Cancelled."
794
- password = prompt_for_password()
795
-
796
- # Try to turn a non-DN into a DN
797
- user = nil
798
- if binddn.index( '=' )
799
- user = Treequel::Branch.new( @dir, binddn )
800
- else
801
- user = @dir.filter( :uid => binddn ).first
802
- end
803
-
804
- @dir.bind( user, password )
805
- message "Bound as #{user}"
806
- end
807
- set_options :bind do |oparser, options|
808
- oparser.banner = "bind [BIND_DN or UID]"
809
- oparser.separator "Bind as BIND_DN or UID"
810
- oparser.separator "If you don't specify a BIND_DN, you will be prompted for it."
811
- end
812
-
813
-
814
- ### Start an IRB session on either the current branchset, if invoked with no arguments, or
815
- ### on a branchset for the specified +rdn+ if one is given.
816
- def irb_command( options, *args )
817
- branch = nil
818
- if args.empty?
819
- branch = @currbranch
820
- else
821
- rdn = args.first
822
- validate_rdns( rdn )
823
- branch = @currbranch.get_child( rdn )
824
- end
825
-
826
- self.log.debug "Setting up IRb shell"
827
- IRB.start_session( branch )
828
- end
829
- set_options :irb do |oparser, options|
830
- oparser.banner = "irb [RDN]"
831
- oparser.separator "Start an IRb shell with either the current branch (if none is " +
832
- "specified) or a branch for the entry specified by the given RDN."
833
- end
834
-
835
-
836
- ### Handle a command from the user that doesn't exist.
837
- def handle_missing_cmd( *args )
838
- command = args.shift || '(testing?)'
839
- message "Unknown command %p" % [ command ]
840
- message "Known commands: ", ' ' + @commands.join(', ')
841
- end
842
-
843
-
844
- ### Find methods that implement commands and return them in a sorted Array.
845
- def find_commands
846
- return self.methods.
847
- collect {|mname| mname.to_s }.
848
- grep( /^(\w+)_command$/ ).
849
- collect {|mname| mname[/^(\w+)_command$/, 1] }.
850
- sort
851
- end
852
-
853
-
854
- #################################################################
855
- ### U T I L I T Y M E T H O D S
856
- #################################################################
857
-
858
- ### Convert the given +patterns+ to branchsets relative to the current branch and return
859
- ### them. This is used to map shell arguments like 'cn=*', 'Hosts', 'cn=dav*' into
860
- ### branchsets that will find matching entries.
861
- def convert_to_branchsets( *patterns )
862
- self.log.debug "Turning %d patterns into branchsets." % [ patterns.length ]
863
- return patterns.collect do |pat|
864
- key, val = pat.split( /\s*=\s*/, 2 )
865
- self.log.debug " making a filter out of %p => %p" % [ key, val ]
866
- @currbranch.filter( key => val )
867
- end
868
- end
869
-
870
-
871
- ### Generate long-form output lines for the 'ls' command for the given +branch+.
872
- def make_longform_ls_output( branch, options )
873
- children = branch.children
874
- totalmsg = "total %d" % [ children.length ]
875
-
876
- # Calcuate column widths
877
- oclen = children.map do |subbranch|
878
- subbranch.include_operational_attrs = true
879
- subbranch[:structuralObjectClass] ? subbranch[:structuralObjectClass].length : 0
880
- end.max
881
-
882
- # Set up sorting by collecting all the requested sort criteria as Proc objects which
883
- # will be applied
884
- sortfuncs = []
885
- sortfuncs << lambda {|subbranch| subbranch[:hasSubordinates] ? 0 : 1 } if options.dirsort
886
- sortfuncs << lambda {|subbranch| subbranch[:modifyTimestamp] } if options.timesort
887
- sortfuncs << lambda {|subbranch| subbranch.rdn.downcase }
888
-
889
- rows = children.
890
- sort_by {|subbranch| sortfuncs.collect {|func| func.call(subbranch) } }.
891
- collect {|subbranch| self.format_description(subbranch, oclen) }
892
-
893
- return [ totalmsg ] + (options.reversesort ? rows.reverse : rows)
894
- end
895
-
896
-
897
- ### Generate short-form 'ls' output for the given +branch+ and return it.
898
- def make_shortform_ls_output( branch, options )
899
- branch.include_operational_attrs = true
900
- entries = branch.children.
901
- collect {|b| b.rdn + (b[:hasSubordinates] ? '/' : '') }.
902
- sort_by {|rdn| rdn.downcase }
903
- self.log.debug "Displaying %d entries in short form." % [ entries.length ]
904
-
905
- return columnize( entries ).gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
906
- format_rdn( rdn )
907
- end
908
- end
909
-
910
-
911
- ### Return the description of the specified +branch+ suitable for displaying in
912
- ### the directory listing. The +oclen+ is the width of the objectclass column.
913
- def format_description( branch, oclen=40 )
914
- rdn = format_rdn( branch.rdn )
915
- metadatalen = oclen + 16 + 6 # oc + timestamp + whitespace
916
- maxdesclen = self.columns - metadatalen - rdn.length - 5
917
-
918
- modtime = branch[:modifyTimestamp] || branch[:createTimestamp]
919
- return "%#{oclen}s %s %s%s %s" % [
920
- branch[:structuralObjectClass] || '',
921
- modtime.strftime('%Y-%m-%d %H:%M'),
922
- rdn,
923
- branch[:hasSubordinates] ? '/' : '',
924
- single_line_description( branch, maxdesclen )
925
- ]
926
- end
927
-
928
-
929
- ### Generate a single-line description from the specified +branch+
930
- def single_line_description( branch, maxlen=80 )
931
- return '' unless branch[:description] && branch[:description].first
932
- desc = branch[:description].join('; ').gsub( /\n+/, '' )
933
- desc[ maxlen..desc.length ] = '...' if desc.length > maxlen
934
- return '(' + desc + ')'
935
- end
936
-
937
-
938
- ### Create a new entry in the directory for the specified +branch+.
939
- def create_new_entry( branch )
940
- raise "#{branch.dn} already exists." if branch.exists?
941
-
942
- # Prompt for the list of included objectClasses and build the appropriate
943
- # blank entry with them in mind.
944
- completions = branch.directory.schema.object_classes.keys.collect {|oid| oid.to_s }
945
- self.log.debug "Prompting for new entry object classes with %d completions." %
946
- [ completions.length ]
947
- object_classes = prompt_for_multiple_values( "Entry objectClasses:", nil, completions ).
948
- collect {|arg| arg.strip }.compact
949
- self.log.debug " user wants %d objectclasses: %p" % [ object_classes.length, object_classes ]
950
-
951
- # Edit the entry
952
- if newhash = edit_in_yaml( branch, object_classes )
953
- branch.create( newhash )
954
- message "Saved #{branch.dn}."
955
- else
956
- error_message "#{branch.dn} not saved."
957
- end
958
- end
959
-
960
-
961
- ### Dump the specified +object+ to a file as YAML, invoke an editor on it, then undump the
962
- ### result. If the file has changed, return the updated object, else returns +nil+.
963
- def edit_in_yaml( object, object_classes=[] )
964
- yaml = branch_as_yaml( object, false, object_classes )
965
- filename = Digest::SHA1.hexdigest( yaml )
966
- tempfile = Tempfile.new( filename )
967
-
968
- self.log.debug "Object as YAML is: %p" % [ yaml ]
969
- tempfile.print( yaml )
970
- tempfile.close
971
-
972
- new_yaml = edit( tempfile.path )
973
-
974
- if new_yaml == yaml
975
- message "Unchanged."
976
- return nil
977
- else
978
- return YAML.load( new_yaml )
979
- end
980
- end
981
-
982
-
983
- ### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
984
- ### include the entry's operational attributes. If +extra_objectclasses+ contains
985
- ### one or more objectClass OIDs, include their MUST and MAY attributes when building the
986
- ### YAML representation of the branch.
987
- def branch_as_yaml( object, include_operational=false, extra_objectclasses=[] )
988
- object.include_operational_attrs = include_operational
989
-
990
- # Make sure the displayed entry has the MUST attributes
991
- entryhash = stringify_keys( object.must_attributes_hash(*extra_objectclasses) )
992
- entryhash.merge!( object.entry || {} )
993
- entryhash.merge!( object.rdn_attributes )
994
- entryhash['objectClass'] ||= []
995
- entryhash['objectClass'] |= extra_objectclasses
996
-
997
- entryhash.delete( 'dn' ) # Special attribute, can't be edited
998
-
999
- yaml = entryhash.to_yaml
1000
- yaml[ 5, 0 ] = "# #{object.dn}\n"
1001
-
1002
- # Make comments out of MAY attributes that are unset
1003
- mayhash = stringify_keys( object.may_attributes_hash(*extra_objectclasses) )
1004
- self.log.debug "MAY hash is: %p" % [ mayhash ]
1005
- mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
1006
- yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
1007
-
1008
- return yaml
1009
- end
1010
-
1011
-
1012
- ### Create a command table that maps command abbreviations to the Method object that
1013
- ### implements it.
1014
- def make_command_table( commands )
1015
- table = commands.abbrev
1016
- table.keys.each do |abbrev|
1017
- mname = table.delete( abbrev )
1018
- table[ abbrev ] = self.method( mname + '_command' )
1019
- end
1020
-
1021
- return table
1022
- end
1023
-
1024
-
1025
- ### Output a header containing the given +text+.
1026
- def header( text )
1027
- header = colorize( text, :underscore, :cyan )
1028
- $stderr.puts( header )
1029
- end
1030
-
1031
-
1032
- ### Output the specified message +parts+.
1033
- def message( *parts )
1034
- $stderr.puts( *parts )
1035
- end
1036
-
1037
-
1038
- ### Output the specified <tt>msg</tt> as an ANSI-colored error message
1039
- ### (white on red).
1040
- def error_message( msg, details='' )
1041
- $stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + ' ' + details
1042
- end
1043
- alias :error :error_message
1044
-
1045
-
1046
- ### Highlight and embed a prompt control character in the given +string+ and return it.
1047
- def make_prompt_string( string )
1048
- return CLEAR_CURRENT_LINE + colorize( 'bold', 'yellow' ) { string + ' ' }
1049
- end
1050
-
1051
-
1052
- ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
1053
- ### return the user's input with leading and trailing spaces removed. If a
1054
- ### test is provided, the prompt will repeat until the test returns true.
1055
- ### An optional failure message can also be passed in.
1056
- def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
1057
- prompt_string.chomp!
1058
- prompt_string << ":" unless /\W$/.match( prompt_string )
1059
- response = nil
1060
-
1061
- begin
1062
- prompt = make_prompt_string( prompt_string )
1063
- response = readline( prompt ) || ''
1064
- response.strip!
1065
- if block_given? && ! yield( response )
1066
- error_message( failure_msg + "\n\n" )
1067
- response = nil
1068
- end
1069
- end while response.nil?
1070
-
1071
- return response
1072
- end
1073
-
1074
-
1075
- ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
1076
- ### substituting the given <tt>default</tt> if the user doesn't input
1077
- ### anything. If a test is provided, the prompt will repeat until the test
1078
- ### returns true. An optional failure message can also be passed in.
1079
- def prompt_with_default( prompt_string, default, failure_msg="Try again." )
1080
- response = nil
1081
-
1082
- begin
1083
- default ||= '~'
1084
- response = prompt( "%s [%s]" % [ prompt_string, default ] )
1085
- response = default.to_s if !response.nil? && response.empty?
1086
-
1087
- self.log.debug "Validating response %p" % [ response ]
1088
-
1089
- # the block is a validator. We need to make sure that the user didn't
1090
- # enter '~', because if they did, it's nil and we should move on. If
1091
- # they didn't, then call the block.
1092
- if block_given? && response != '~' && ! yield( response )
1093
- error_message( failure_msg + "\n\n" )
1094
- response = nil
1095
- end
1096
- end while response.nil?
1097
-
1098
- return nil if response == '~'
1099
- return response
1100
- end
1101
-
1102
-
1103
- ### Prompt for an array of values
1104
- def prompt_for_multiple_values( label, default=nil, completions=[] )
1105
- old_completion_proc = nil
1106
-
1107
- message( MULTILINE_PROMPT % [label] )
1108
- if default
1109
- message "Enter a single blank line to keep the default:\n %p" % [ default ]
1110
- end
1111
-
1112
- results = []
1113
- result = nil
1114
-
1115
- if !completions.empty?
1116
- self.log.debug "Prompting with %d completions." % [ completions.length ]
1117
- old_completion_proc = Readline.completion_proc
1118
- Readline.completion_proc = Proc.new do |input|
1119
- completions.flatten.grep( /^#{Regexp.quote(input)}/i ).sort
1120
- end
1121
- end
1122
-
1123
- begin
1124
- result = readline( make_prompt_string("> ") )
1125
- if result.nil? || result.empty?
1126
- results << default if default && results.empty?
1127
- else
1128
- results << result
1129
- end
1130
- end until result.nil? || result.empty?
1131
-
1132
- return results.flatten
1133
- ensure
1134
- Readline.completion_proc = old_completion_proc if old_completion_proc
1135
- end
1136
-
1137
-
1138
- ### Turn echo and masking of input on/off.
1139
- def noecho( masked=false )
1140
- rval = nil
1141
- term = Termios.getattr( $stdin )
1142
-
1143
- begin
1144
- newt = term.dup
1145
- newt.c_lflag &= ~Termios::ECHO
1146
- newt.c_lflag &= ~Termios::ICANON if masked
1147
-
1148
- Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
1149
-
1150
- rval = yield
1151
- ensure
1152
- Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
1153
- end
1154
-
1155
- return rval
1156
- end
1157
-
1158
-
1159
- ### Prompt the user for her password, turning off echo if the 'termios' module is
1160
- ### available.
1161
- def prompt_for_password( prompt="Password: " )
1162
- rval = nil
1163
- noecho( true ) do
1164
- $stderr.print( prompt )
1165
- rval = ($stdin.gets || '').chomp
1166
- end
1167
- $stderr.puts
1168
- return rval
1169
- end
1170
-
1171
-
1172
- ### Display a description of a potentially-dangerous task, and prompt
1173
- ### for confirmation. If the user answers with anything that begins
1174
- ### with 'y', yield to the block. If +abort_on_decline+ is +true+,
1175
- ### any non-'y' answer will fail with an error message.
1176
- def ask_for_confirmation( description, abort_on_decline=true )
1177
- puts description
1178
-
1179
- answer = prompt_with_default( "Continue?", 'n' ) do |input|
1180
- input =~ /^[yn]/i
1181
- end
1182
-
1183
- if answer =~ /^y/i
1184
- return yield
1185
- elsif abort_on_decline
1186
- error "Aborted."
1187
- fail
1188
- end
1189
-
1190
- return false
1191
- end
1192
- alias :prompt_for_confirmation :ask_for_confirmation
1193
-
1194
-
1195
- ### Invoke the user's editor on the given +filename+ and return the exit code
1196
- ### from doing so.
1197
- def edit( filename )
1198
- editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
1199
- system editor, filename.to_s
1200
- unless $?.success? || editor =~ /vim/i
1201
- raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
1202
- end
1203
- return File.read( filename )
1204
- end
1205
-
1206
-
1207
- ### Make an easily-comparable version vector out of +ver+ and return it.
1208
- def vvec( ver )
1209
- return ver.split('.').collect {|char| char.to_i }.pack('N*')
1210
- end
1211
-
1212
-
1213
- ### Raise a RuntimeError if the specified +rdn+ is invalid.
1214
- def validate_rdns( *rdns )
1215
- rdns.flatten.each do |rdn|
1216
- raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
1217
- end
1218
- end
1219
-
1220
-
1221
- ### Return an ANSI-colored version of the given +rdn+ string.
1222
- def format_rdn( rdn )
1223
- rdn.split( /,/ ).collect do |rdn_part|
1224
- key, val = rdn_part.split( /\s*=\s*/, 2 )
1225
- colorize( :white ) { key } +
1226
- colorize( :bold, :black ) { '=' } +
1227
- colorize( :bold, :white ) { val }
1228
- end.join( colorize(',', :green) )
1229
- end
1230
-
1231
-
1232
- ### Highlight LDIF and return it.
1233
- def format_ldif( ldif )
1234
- self.log.debug "Formatting LDIF: %p" % [ ldif ]
1235
- return ldif.gsub( LDIF_ATTRVAL_SPEC ) do
1236
- key, val = $1, $2.strip
1237
- self.log.debug " formatting attribute: [ %p, %p ], remainder: %p" %
1238
- [ key, val, $POSTMATCH ]
1239
-
1240
- case val
1241
-
1242
- # Base64-encoded value
1243
- when /^:/
1244
- val = val[1..-1].strip
1245
- key +
1246
- colorize( :dark, :green ) { ':: ' } +
1247
- colorize( :green ) { val } + "\n"
1248
-
1249
- # URL
1250
- when /^</
1251
- val = val[1..-1].strip
1252
- key +
1253
- colorize( :dark, :yellow ) { ':< ' } +
1254
- colorize( :yellow ) { val } + "\n"
1255
-
1256
- # Regular attribute
1257
- else
1258
- key +
1259
- colorize( :dark, :white ) { ': ' } +
1260
- colorize( :bold, :white ) { val } + "\n"
1261
- end
1262
- end
1263
- end
1264
-
1265
-
1266
- ### Return the specified +entries+ as an Array of span-sorted columns fit to the
1267
- ### current terminal width.
1268
- def columnize( *entries )
1269
- return Columnize.columnize( entries.flatten, @columns, ' ' )
1270
- end
1271
-
1272
- end # class Treequel::Shell
1273
-
1274
-
1275
- Treequel::Shell.run( ARGV.dup )
1276
-