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 +2 -1
- data/ChangeLog +61 -13
- data/History.rdoc +5 -0
- data/Manifest.txt +0 -3
- data/README.rdoc +10 -0
- data/Rakefile +6 -25
- data/lib/treequel.rb +2 -2
- data/lib/treequel/branch.rb +2 -2
- metadata +8 -79
- metadata.gz.sig +0 -0
- data/bin/treeirb +0 -18
- data/bin/treequel +0 -1276
- data/bin/treewhat +0 -394
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]
|
|
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]
|
|
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
data/Manifest.txt
CHANGED
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 '
|
|
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
|
-
|
|
47
|
-
"
|
|
48
|
-
'',
|
|
49
|
-
"
|
|
50
|
-
|
|
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.
|
|
35
|
+
VERSION = '1.10.0'
|
|
36
36
|
|
|
37
37
|
# VCS revision
|
|
38
|
-
REVISION = %q$Revision:
|
|
38
|
+
REVISION = %q$Revision: 0fa0df1acab0 $
|
|
39
39
|
|
|
40
40
|
# Common paths for ldap.conf
|
|
41
41
|
COMMON_LDAP_CONF_PATHS = %w[
|
data/lib/treequel/branch.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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.
|
|
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.
|
|
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: ! "
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|