treequel 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data.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
-