treequel 1.0.1 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/ChangeLog +176 -14
  2. data/LICENSE +1 -1
  3. data/Rakefile +61 -45
  4. data/Rakefile.local +20 -0
  5. data/bin/treequel +502 -269
  6. data/examples/ldap-rack-auth.rb +2 -0
  7. data/lib/treequel.rb +221 -18
  8. data/lib/treequel/branch.rb +410 -201
  9. data/lib/treequel/branchcollection.rb +25 -13
  10. data/lib/treequel/branchset.rb +42 -40
  11. data/lib/treequel/constants.rb +233 -3
  12. data/lib/treequel/control.rb +95 -0
  13. data/lib/treequel/controls/contentsync.rb +138 -0
  14. data/lib/treequel/controls/pagedresults.rb +162 -0
  15. data/lib/treequel/controls/sortedresults.rb +216 -0
  16. data/lib/treequel/directory.rb +212 -65
  17. data/lib/treequel/exceptions.rb +11 -12
  18. data/lib/treequel/filter.rb +1 -12
  19. data/lib/treequel/mixins.rb +83 -47
  20. data/lib/treequel/monkeypatches.rb +29 -0
  21. data/lib/treequel/schema.rb +23 -19
  22. data/lib/treequel/schema/attributetype.rb +33 -3
  23. data/lib/treequel/schema/ldapsyntax.rb +0 -11
  24. data/lib/treequel/schema/matchingrule.rb +0 -11
  25. data/lib/treequel/schema/matchingruleuse.rb +0 -11
  26. data/lib/treequel/schema/objectclass.rb +36 -10
  27. data/lib/treequel/schema/table.rb +159 -0
  28. data/lib/treequel/sequel_integration.rb +7 -7
  29. data/lib/treequel/utils.rb +4 -66
  30. data/rake/documentation.rb +89 -0
  31. data/rake/helpers.rb +375 -307
  32. data/rake/hg.rb +16 -2
  33. data/rake/manual.rb +11 -6
  34. data/rake/packaging.rb +20 -35
  35. data/rake/publishing.rb +22 -62
  36. data/spec/lib/constants.rb +20 -0
  37. data/spec/lib/control_behavior.rb +44 -0
  38. data/spec/lib/matchers.rb +51 -0
  39. data/spec/treequel/branch_spec.rb +88 -29
  40. data/spec/treequel/branchcollection_spec.rb +24 -1
  41. data/spec/treequel/branchset_spec.rb +123 -51
  42. data/spec/treequel/control_spec.rb +48 -0
  43. data/spec/treequel/controls/contentsync_spec.rb +38 -0
  44. data/spec/treequel/controls/pagedresults_spec.rb +138 -0
  45. data/spec/treequel/controls/sortedresults_spec.rb +171 -0
  46. data/spec/treequel/directory_spec.rb +186 -16
  47. data/spec/treequel/mixins_spec.rb +42 -3
  48. data/spec/treequel/schema/attributetype_spec.rb +22 -20
  49. data/spec/treequel/schema/objectclass_spec.rb +67 -46
  50. data/spec/treequel/schema/table_spec.rb +134 -0
  51. data/spec/treequel_spec.rb +277 -15
  52. metadata +89 -108
  53. data/bin/treequel.orig +0 -963
  54. data/examples/ldap-monitor.rb +0 -143
  55. data/examples/ldap-monitor/public/css/master.css +0 -328
  56. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  57. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  58. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  59. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  60. data/examples/ldap-monitor/public/images/plug.png +0 -0
  61. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  62. data/examples/ldap-monitor/public/images/tick.png +0 -0
  63. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  64. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  65. data/examples/ldap-monitor/views/backends.erb +0 -41
  66. data/examples/ldap-monitor/views/connections.erb +0 -74
  67. data/examples/ldap-monitor/views/databases.erb +0 -39
  68. data/examples/ldap-monitor/views/dump_subsystem.erb +0 -14
  69. data/examples/ldap-monitor/views/index.erb +0 -14
  70. data/examples/ldap-monitor/views/layout.erb +0 -35
  71. data/examples/ldap-monitor/views/listeners.erb +0 -30
  72. data/rake/rdoc.rb +0 -30
  73. data/rake/win32.rb +0 -190
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: treequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 4
9
+ version: 1.0.4
5
10
  platform: ruby
6
11
  authors:
7
12
  - Michael Granger, Mahlon E. Smith
@@ -9,49 +14,23 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2009-11-19 00:00:00 -08:00
17
+ date: 2010-07-07 00:00:00 -07:00
13
18
  default_executable:
14
19
  dependencies:
15
- - !ruby/object:Gem::Dependency
16
- name: columnize
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 0.3.1
24
- version:
25
- - !ruby/object:Gem::Dependency
26
- name: termios
27
- type: :runtime
28
- version_requirement:
29
- version_requirements: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: 0.9.4
34
- version:
35
- - !ruby/object:Gem::Dependency
36
- name: ruby-terminfo
37
- type: :runtime
38
- version_requirement:
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: 0.1.1
44
- version:
45
20
  - !ruby/object:Gem::Dependency
46
21
  name: ruby-ldap
47
- type: :runtime
48
- version_requirement:
49
- version_requirements: !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
50
24
  requirements:
51
25
  - - ">="
52
26
  - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 9
30
+ - 9
53
31
  version: 0.9.9
54
- version:
32
+ type: :runtime
33
+ version_requirements: *id001
55
34
  description: |-
56
35
  A library for interacting with LDAP (modeled after Sequel). It is an attempt to remove
57
36
  the impedence mismatch of trying to treat LDAP as if it's a relational database, and
@@ -60,7 +39,6 @@ email:
60
39
  - mahlon@martini.nu
61
40
  - ged@FaerieMUD.org
62
41
  executables:
63
- - treequel.orig
64
42
  - treeirb
65
43
  - treequel
66
44
  extensions: []
@@ -74,129 +52,132 @@ files:
74
52
  - ChangeLog
75
53
  - README
76
54
  - LICENSE
77
- - spec/treequel_spec.rb
55
+ - spec/treequel/branch_spec.rb
56
+ - spec/treequel/branchcollection_spec.rb
57
+ - spec/treequel/branchset_spec.rb
58
+ - spec/treequel/control_spec.rb
59
+ - spec/treequel/controls/contentsync_spec.rb
60
+ - spec/treequel/controls/pagedresults_spec.rb
61
+ - spec/treequel/controls/sortedresults_spec.rb
62
+ - spec/treequel/directory_spec.rb
63
+ - spec/treequel/filter_spec.rb
64
+ - spec/treequel/mixins_spec.rb
78
65
  - spec/treequel/schema/attributetype_spec.rb
79
66
  - spec/treequel/schema/ldapsyntax_spec.rb
80
- - spec/treequel/schema/matchingruleuse_spec.rb
81
67
  - spec/treequel/schema/matchingrule_spec.rb
68
+ - spec/treequel/schema/matchingruleuse_spec.rb
82
69
  - spec/treequel/schema/objectclass_spec.rb
70
+ - spec/treequel/schema/table_spec.rb
83
71
  - spec/treequel/schema_spec.rb
84
- - spec/treequel/filter_spec.rb
85
72
  - spec/treequel/utils_spec.rb
86
- - spec/treequel/branch_spec.rb
87
- - spec/treequel/directory_spec.rb
88
- - spec/treequel/branchset_spec.rb
89
- - spec/treequel/mixins_spec.rb
90
- - spec/treequel/branchcollection_spec.rb
73
+ - spec/treequel_spec.rb
91
74
  - spec/lib/constants.rb
75
+ - spec/lib/control_behavior.rb
92
76
  - spec/lib/helpers.rb
93
- - bin/treequel.orig
77
+ - spec/lib/matchers.rb
94
78
  - bin/treeirb
95
79
  - bin/treequel
96
- - lib/treequel.rb
97
- - lib/treequel/schema/matchingrule.rb
80
+ - lib/treequel/branch.rb
81
+ - lib/treequel/branchcollection.rb
82
+ - lib/treequel/branchset.rb
83
+ - lib/treequel/constants.rb
84
+ - lib/treequel/control.rb
85
+ - lib/treequel/controls/contentsync.rb
86
+ - lib/treequel/controls/pagedresults.rb
87
+ - lib/treequel/controls/sortedresults.rb
88
+ - lib/treequel/directory.rb
89
+ - lib/treequel/exceptions.rb
90
+ - lib/treequel/filter.rb
91
+ - lib/treequel/mixins.rb
92
+ - lib/treequel/monkeypatches.rb
98
93
  - lib/treequel/schema/attributetype.rb
99
94
  - lib/treequel/schema/ldapsyntax.rb
95
+ - lib/treequel/schema/matchingrule.rb
100
96
  - lib/treequel/schema/matchingruleuse.rb
101
97
  - lib/treequel/schema/objectclass.rb
102
- - lib/treequel/directory.rb
103
- - lib/treequel/exceptions.rb
104
- - lib/treequel/branchcollection.rb
105
- - lib/treequel/mixins.rb
106
- - lib/treequel/utils.rb
98
+ - lib/treequel/schema/table.rb
107
99
  - lib/treequel/schema.rb
108
- - lib/treequel/constants.rb
109
- - lib/treequel/branchset.rb
110
- - lib/treequel/filter.rb
111
- - lib/treequel/branch.rb
112
100
  - lib/treequel/sequel_integration.rb
113
- - rake/style.rb
114
- - rake/rdoc.rb
115
- - rake/hg.rb
116
- - rake/svn.rb
117
- - rake/packaging.rb
118
- - rake/publishing.rb
101
+ - lib/treequel/utils.rb
102
+ - lib/treequel.rb
119
103
  - rake/191_compat.rb
120
104
  - rake/dependencies.rb
121
- - rake/win32.rb
122
- - rake/manual.rb
123
- - rake/verifytask.rb
105
+ - rake/documentation.rb
124
106
  - rake/helpers.rb
107
+ - rake/hg.rb
108
+ - rake/manual.rb
109
+ - rake/packaging.rb
110
+ - rake/publishing.rb
111
+ - rake/style.rb
112
+ - rake/svn.rb
125
113
  - rake/testing.rb
126
- - ./examples/ldap_state.rb
114
+ - rake/verifytask.rb
127
115
  - ./examples/company-directory.rb
128
- - ./examples/ldap-monitor.rb
129
116
  - ./examples/ldap-rack-auth.rb
130
- - ./examples/ldap-monitor/views/backends.erb
131
- - ./examples/ldap-monitor/views/connections.erb
132
- - ./examples/ldap-monitor/views/listeners.erb
133
- - ./examples/ldap-monitor/views/databases.erb
134
- - ./examples/ldap-monitor/views/index.erb
135
- - ./examples/ldap-monitor/views/dump_subsystem.erb
136
- - ./examples/ldap-monitor/views/layout.erb
137
- - ./examples/ldap-monitor/public/css/master.css
138
- - ./examples/ldap-monitor/public/images/plug.png
139
- - ./examples/ldap-monitor/public/images/shadows/large-30-down.png
140
- - ./examples/ldap-monitor/public/images/globe_small.png
141
- - ./examples/ldap-monitor/public/images/chain_small.png
142
- - ./examples/ldap-monitor/public/images/tick_circle.png
143
- - ./examples/ldap-monitor/public/images/globe_small_green.png
144
- - ./examples/ldap-monitor/public/images/card_small.png
145
- - ./examples/ldap-monitor/public/images/treequel-favicon.png
146
- - ./examples/ldap-monitor/public/images/tick.png
117
+ - ./examples/ldap_state.rb
147
118
  - Rakefile.local
148
119
  has_rdoc: true
149
120
  homepage: http://deveiate.org/projects/Treequel
150
121
  licenses: []
151
122
 
152
- post_install_message:
123
+ post_install_message: |-
124
+ If you want to use the included 'treequel' LDAP shell, you'll need to install
125
+ the following libraries as well:
126
+ - termios
127
+ - ruby-terminfo
128
+ - columnize
129
+ - diff-lcs
153
130
  rdoc_options:
154
- - -w
155
- - "4"
156
- - -HN
157
- - -i
131
+ - --tab-width=4
132
+ - --show-hash
133
+ - --include
158
134
  - .
159
- - -m
160
- - README
161
- - -t
162
- - treequel
163
- - -W
164
- - http://deveiate.org/projects/Treequel/browser/
135
+ - --main=README
136
+ - --title=treequel
165
137
  require_paths:
166
138
  - lib
167
139
  required_ruby_version: !ruby/object:Gem::Requirement
168
140
  requirements:
169
141
  - - ">="
170
142
  - !ruby/object:Gem::Version
143
+ segments:
144
+ - 0
171
145
  version: "0"
172
- version:
173
146
  required_rubygems_version: !ruby/object:Gem::Requirement
174
147
  requirements:
175
148
  - - ">="
176
149
  - !ruby/object:Gem::Version
150
+ segments:
151
+ - 0
177
152
  version: "0"
178
- version:
179
153
  requirements:
180
154
  - ldap >=0
181
- rubyforge_project: deveiate
182
- rubygems_version: 1.3.5
155
+ rubyforge_project:
156
+ rubygems_version: 1.3.6
183
157
  signing_key:
184
158
  specification_version: 3
185
159
  summary: An honest LDAP library
186
160
  test_files:
187
- - spec/treequel_spec.rb
161
+ - spec/treequel/branch_spec.rb
162
+ - spec/treequel/branchcollection_spec.rb
163
+ - spec/treequel/branchset_spec.rb
164
+ - spec/treequel/control_spec.rb
165
+ - spec/treequel/controls/contentsync_spec.rb
166
+ - spec/treequel/controls/pagedresults_spec.rb
167
+ - spec/treequel/controls/sortedresults_spec.rb
168
+ - spec/treequel/directory_spec.rb
169
+ - spec/treequel/filter_spec.rb
170
+ - spec/treequel/mixins_spec.rb
188
171
  - spec/treequel/schema/attributetype_spec.rb
189
172
  - spec/treequel/schema/ldapsyntax_spec.rb
190
- - spec/treequel/schema/matchingruleuse_spec.rb
191
173
  - spec/treequel/schema/matchingrule_spec.rb
174
+ - spec/treequel/schema/matchingruleuse_spec.rb
192
175
  - spec/treequel/schema/objectclass_spec.rb
176
+ - spec/treequel/schema/table_spec.rb
193
177
  - spec/treequel/schema_spec.rb
194
- - spec/treequel/filter_spec.rb
195
178
  - spec/treequel/utils_spec.rb
196
- - spec/treequel/branch_spec.rb
197
- - spec/treequel/directory_spec.rb
198
- - spec/treequel/branchset_spec.rb
199
- - spec/treequel/mixins_spec.rb
200
- - spec/treequel/branchcollection_spec.rb
179
+ - spec/treequel_spec.rb
201
180
  - spec/lib/constants.rb
181
+ - spec/lib/control_behavior.rb
202
182
  - spec/lib/helpers.rb
183
+ - spec/lib/matchers.rb
data/bin/treequel.orig DELETED
@@ -1,963 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'rubygems'
4
-
5
- require 'abbrev'
6
- require 'columnize'
7
- require 'digest/sha1'
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
-
20
- require 'treequel'
21
- require 'treequel/mixins'
22
- require 'treequel/constants'
23
-
24
-
25
- ### Monkeypatch for resetting an OpenStruct's state.
26
- class OpenStruct
27
-
28
- ### Clear all defined fields and values.
29
- def clear
30
- @table.clear
31
- end
32
-
33
- end
34
-
35
-
36
- # The Treequel shell.
37
- class Treequel::Shell
38
- include Readline,
39
- Treequel::Loggable,
40
- Treequel::Constants::Patterns
41
-
42
- # Set some ANSI escape code constants (Shamelessly stolen from Perl's
43
- # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
44
- ANSI_ATTRIBUTES = {
45
- 'clear' => 0,
46
- 'reset' => 0,
47
- 'bold' => 1,
48
- 'dark' => 2,
49
- 'underline' => 4,
50
- 'underscore' => 4,
51
- 'blink' => 5,
52
- 'reverse' => 7,
53
- 'concealed' => 8,
54
-
55
- 'black' => 30, 'on_black' => 40,
56
- 'red' => 31, 'on_red' => 41,
57
- 'green' => 32, 'on_green' => 42,
58
- 'yellow' => 33, 'on_yellow' => 43,
59
- 'blue' => 34, 'on_blue' => 44,
60
- 'magenta' => 35, 'on_magenta' => 45,
61
- 'cyan' => 36, 'on_cyan' => 46,
62
- 'white' => 37, 'on_white' => 47
63
- }
64
-
65
-
66
- # Prompt text for #prompt_for_multiple_values
67
- MULTILINE_PROMPT = <<-'EOF'
68
- Enter one or more values for '%s'.
69
- A blank line finishes input.
70
- EOF
71
-
72
- # Some ANSI codes for fancier stuff
73
- CLEAR_TO_EOL = "\e[K"
74
- CLEAR_CURRENT_LINE = "\e[2K"
75
-
76
- # Log levels
77
- LOG_LEVELS = {
78
- 'debug' => Logger::DEBUG,
79
- 'info' => Logger::INFO,
80
- 'warn' => Logger::WARN,
81
- 'error' => Logger::ERROR,
82
- 'fatal' => Logger::FATAL,
83
- }.freeze
84
- LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
85
-
86
- # Command option parsers
87
- OPTION_PARSERS = {}
88
-
89
- # Path to the default history file
90
- HISTORY_FILE = Pathname( "~/.treequel.history" )
91
-
92
- # Number of items to store in history by default
93
- DEFAULT_HISTORY_SIZE = 100
94
-
95
-
96
- #################################################################
97
- ### C L A S S M E T H O D S
98
- #################################################################
99
-
100
- ### Create an option parser from the specified +block+ for the given +command+ and register
101
- ### it. Many thanks to apeiros and dominikh on #Ruby-Pro for the ideas behind this.
102
- def self::set_options( command, &block )
103
- options = OpenStruct.new
104
- oparser = OptionParser.new( "Help for #{command}" ) do |o|
105
- yield( o, options )
106
- end
107
- oparser.default_argv = []
108
-
109
- OPTION_PARSERS[command.to_sym] = [oparser, options]
110
- end
111
-
112
-
113
- #################################################################
114
- ### I N S T A N C E M E T H O D S
115
- #################################################################
116
-
117
- ### Create a new shell that will traverse the directory at the specified +uri+.
118
- def initialize( uri )
119
- Treequel.logger.level = Logger::WARN
120
- Treequel::Branch.include_operational_attrs = true
121
-
122
- @uri = uri
123
- @quit = false
124
- @dir = Treequel.directory( @uri )
125
- @currbranch = @dir
126
- @columns = TermInfo.screen_width
127
- @rows = TermInfo.screen_height
128
-
129
- @commands = self.find_commands
130
- @completions = @commands.abbrev
131
- @command_table = make_command_table( @commands )
132
- end
133
-
134
-
135
- ### The command loop: run the shell until the user wants to quit
136
- def run
137
- message "Connected to %s" % [ @uri ]
138
-
139
- # Set up the completion callback
140
- self.setup_completion
141
-
142
- # Load saved command-line history
143
- self.read_history
144
-
145
- # Run until something sets the quit flag
146
- until @quit
147
- $stderr.puts
148
- prompt = make_prompt_string( @currbranch.dn + '> ' )
149
- Readline.basic_word_break_characters = ''
150
- input = Readline.readline( prompt, true )
151
- self.log.debug "Input is: %p" % [ input ]
152
-
153
- # EOL makes the shell quit
154
- if input.nil?
155
- self.log.debug "EOL: setting quit flag"
156
- @quit = true
157
-
158
- # Blank input -- just reprompt
159
- elsif input == ''
160
- self.log.debug "No command. Re-displaying the prompt."
161
-
162
- # Parse everything else into command + everything else
163
- else
164
- self.log.debug "Dispatching input: %p" % [ input ]
165
- self.dispatch_cmd( input )
166
- end
167
- end
168
-
169
- message "\nSaving history...\n"
170
- self.save_history
171
-
172
- message "done."
173
- end
174
-
175
-
176
- ### Parse the specified +input+ into a command, options, and arguments and dispatch them
177
- ### to the appropriate command method.
178
- def dispatch_cmd( input )
179
- command, *args = Shellwords.shellwords( input )
180
-
181
- # If it's a valid command, run it
182
- if meth = @command_table[ command ]
183
- full_command = @completions[ command ].to_sym
184
-
185
- # If there's a registered optionparser for the command, use it to
186
- # split out options and arguments, then pass those to the command.
187
- if OPTION_PARSERS.key?( full_command )
188
- oparser, options = OPTION_PARSERS[ full_command ]
189
- self.log.debug "Got an option-parser for #{full_command}."
190
-
191
- cmdargs = oparser.parse( args )
192
- self.log.debug " options=%p, args=%p" % [ options, cmdargs ]
193
- meth.call( options, *cmdargs )
194
-
195
- options.clear
196
-
197
- # ...otherwise just call it with all the args.
198
- else
199
- meth.call( *args )
200
- end
201
-
202
- # ...otherwise call the fallback handler
203
- else
204
- self.handle_missing_cmd( command )
205
- end
206
-
207
- rescue => err
208
- error_message( err.class.name, err.message )
209
- err.backtrace.each do |frame|
210
- self.log.debug " " + frame
211
- end
212
- end
213
-
214
-
215
- #########
216
- protected
217
- #########
218
-
219
- ### Set up Readline completion
220
- def setup_completion
221
- Readline.completion_proc = self.method( :completion_callback ).to_proc
222
- Readline.completer_word_break_characters = ''
223
- end
224
-
225
-
226
- ### Read command line history from HISTORY_FILE
227
- def read_history
228
- histfile = HISTORY_FILE.expand_path
229
-
230
- if histfile.exist?
231
- lines = histfile.readlines.collect {|line| line.chomp }
232
- self.log.debug "Read %d saved history commands from %s." % [ lines.nitems, histfile ]
233
- Readline::HISTORY.push( *lines )
234
- else
235
- self.log.debug "History file '%s' was empty or non-existant." % [ histfile ]
236
- end
237
- end
238
-
239
-
240
- ### Save command line history to HISTORY_FILE
241
- def save_history
242
- histfile = HISTORY_FILE.expand_path
243
-
244
- lines = Readline::HISTORY.to_a.reverse.uniq.reverse
245
- lines = lines[ -DEFAULT_HISTORY_SIZE, DEFAULT_HISTORY_SIZE ] if
246
- lines.nitems > DEFAULT_HISTORY_SIZE
247
-
248
- self.log.debug "Saving %d history lines to %s." % [ lines.length, histfile ]
249
-
250
- histfile.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
251
- ofh.puts( *lines )
252
- end
253
- end
254
-
255
-
256
- ### Handle completion requests from Readline.
257
- def completion_callback( input )
258
- self.log.debug "Input completion: %p" % [ input ]
259
- parts = Shellwords.shellwords( input )
260
-
261
- # If there aren't any arguments, it's command completion
262
- if parts.length == 1
263
- # One completion means it's an unambiguous match, so just complete it.
264
- possible_completions = @commands.grep( /^#{Regexp.quote(input)}/ ).sort
265
- self.log.debug " possible completions: %p" % [ possible_completions ]
266
- return possible_completions
267
- else
268
- incomplete = parts.pop
269
- possible_completions = @currbranch.children.
270
- collect {|br| br.rdn }.grep( /^#{Regexp.quote(incomplete)}/i ).sort
271
-
272
- possible_completions.map! do |lastpart|
273
- parts.join( ' ' ) + ' ' + lastpart
274
- end
275
- end
276
- end
277
-
278
-
279
- #################################################################
280
- ### C O M M A N D S
281
- #################################################################
282
-
283
- ### Show the completions hash
284
- def show_completions_command
285
- message "Completions:", @completions.inspect
286
- end
287
-
288
-
289
- ### Show help text for the specified command, or a list of all available commands
290
- ### if none is specified.
291
- def help_command( *args )
292
- if args.empty?
293
- $stderr.puts
294
- message colorize( "Available commands", :bold, :white ),
295
- *columnize(@commands)
296
- else
297
- cmd = args.shift.to_sym
298
- if OPTION_PARSERS.key?( cmd )
299
- oparser, _ = OPTION_PARSERS[ cmd ]
300
- self.log.debug "Setting summary width to: %p" % [ @columns ]
301
- oparser.summary_width = @columns
302
- output = oparser.to_s.sub( /^(.*?)\n/ ) do |match|
303
- colorize( :bold, :white ) { match }
304
- end
305
-
306
- $stderr.puts
307
- message( output )
308
- else
309
- error_message( "No help for '#{cmd}'" )
310
- end
311
- end
312
- end
313
-
314
-
315
- ### Quit the shell.
316
- def quit_command( *args )
317
- message "Okay, exiting."
318
- @quit = true
319
- end
320
-
321
-
322
- ### Set the logging level (if invoked with an argument) or display the current
323
- ### level (with no argument).
324
- def log_command( *args )
325
- newlevel = args.shift
326
- if newlevel
327
- if LOG_LEVELS.key?( newlevel )
328
- Treequel.logger.level = LOG_LEVELS[ newlevel ]
329
- message "Set log level to: %s" % [ newlevel ]
330
- else
331
- levelnames = LOG_LEVEL_NAMES.keys.sort.join(', ')
332
- raise "Invalid log level %p: valid values are:\n %s" % [ newlevel, levelnames ]
333
- end
334
- else
335
- message "Log level is currently: %s" %
336
- [ LOG_LEVEL_NAMES[Treequel.logger.level] ]
337
- end
338
- end
339
-
340
-
341
- ### Display LDIF for the specified RDNs.
342
- def cat_command( *args )
343
- args.each do |rdn|
344
- branch = @currbranch.get_child( rdn )
345
- message( format_ldif(branch.to_ldif) )
346
- end
347
- end
348
-
349
-
350
- ### List the children of the branch specified by the given +rdn+, or the current branch if none
351
- ### are specified.
352
- def ls_command( options, *args )
353
- targets = []
354
-
355
- # No argument, just use the current branch
356
- if args.empty?
357
- targets << @currbranch
358
-
359
- # Otherwise, list each one specified
360
- else
361
- args.each do |rdn|
362
- if branch = @currbranch.get_child( rdn )
363
- targets << branch
364
- else
365
- error_message( "cannot access #{rdn}: no such entry" )
366
- end
367
- end
368
- end
369
-
370
- # Fetch each branch's children, sort them, format them in columns, and highlight them
371
- targets.each do |branch|
372
- if options.longform
373
- message self.make_longform_ls_output( branch, options )
374
- else
375
- message self.make_shortform_ls_output( branch, options )
376
- end
377
- end
378
- end
379
- set_options :ls do |oparser, options|
380
- oparser.banner = "ls [OPTIONS] [DNs]"
381
-
382
- oparser.on( "-l", "--long", FalseClass, "List in long format." ) do
383
- options.longform = true
384
- end
385
-
386
- end
387
-
388
-
389
- ### Generate long-form output lines for the 'ls' command for the given +branch+.
390
- def make_longform_ls_output( branch, options )
391
- rows = []
392
- children = branch.children
393
- rows << colorize( :underscore, :cyan ) { "total %d" % [children.length] }
394
-
395
- # Calcuate column widths
396
- oclen = children.map do |branch|
397
- branch.include_operational_attrs = true
398
- branch[:structuralObjectClass].length
399
- end.max
400
- moddnlen = children.map do |branch|
401
- branch[:modifiersName].length
402
- end.max
403
-
404
- children.
405
- sort_by {|branch| branch.rdn.downcase }.
406
- each do |branch|
407
- # -rw-r--r-- 2 mgranger staff 979 2009-07-27 11:55 Rakefile.local
408
- #
409
- # modifiersName: cn=admin,dc=laika,dc=com
410
- # hasSubordinates: TRUE
411
- # modifyTimestamp: 20090520232650Z
412
- # structuralObjectClass: organizationalUnit
413
- rows << "%#{oclen}s %#{moddnlen}s %s %s%s" % [
414
- branch[:structuralObjectClass],
415
- branch[:modifiersName],
416
- branch[:modifyTimestamp].strftime('%Y-%m-%d %H:%M'),
417
- format_rdn( branch.rdn ),
418
- branch[:hasSubordinates] ? '/' : ''
419
- ]
420
- end
421
-
422
- return rows
423
- end
424
-
425
-
426
- ### Generate short-form 'ls' output for the given +branch+ and return it.
427
- def make_shortform_ls_output( branch, options )
428
- entries = branch.children.
429
- collect {|b| b.rdn }.
430
- sort_by {|rdn| rdn.downcase }
431
- return columnize( entries ).
432
- collect do |row|
433
- row.gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
434
- format_rdn( rdn )
435
- end
436
- end
437
- end
438
-
439
-
440
- ### Change the current working DN to +rdn+.
441
- def cdn_command( rdn=nil, *args )
442
- if rdn.nil?
443
- @currbranch = @dir.base
444
- return
445
- end
446
-
447
- return self.parent_command if rdn == '..'
448
-
449
- raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
450
-
451
- pairs = rdn.split( /\s*,\s*/ )
452
- pairs.each do |dnpair|
453
- self.log.debug " cd to %p" % [ dnpair ]
454
- attribute, value = dnpair.split( /=/, 2 )
455
- self.log.debug " changing to %s( %p )" % [ attribute, value ]
456
- @currbranch = @currbranch.send( attribute, value )
457
- end
458
- end
459
-
460
-
461
- ### Change the current working DN to the current entry's parent.
462
- def parent_command( *args )
463
- parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]
464
-
465
- self.log.debug " changing to %s" % [ parent.dn ]
466
- @currbranch = parent
467
- end
468
-
469
-
470
- ### Edit the entry specified by +rdn+.
471
- def edit_command( options, rdn )
472
- branch = @currbranch.get_child( rdn )
473
- entryhash = nil
474
-
475
- if options.newentry
476
- raise "#{branch.dn} already exists." if branch.exists?
477
- object_classes = prompt_for_multiple_values( "Entry objectClasses:" )
478
- entryhash = branch.valid_attributes_hash( *object_classes )
479
- newhash = edit_in_yaml( entryhash )
480
- args = object_classes + [newhash]
481
- branch.create( *args )
482
- else
483
- raise "#{branch.dn}: no such entry. Did you mean to create it with -n?" unless
484
- branch.exists?
485
- entryhash = branch.entry
486
- newhash = edit_in_yaml( entryhash )
487
- branch.merge( entryhash )
488
- end
489
-
490
- message "Saved #{rdn}."
491
- end
492
- set_options :edit do |oparser, options|
493
- oparser.banner = "edit [OPTIONS] DN"
494
-
495
- oparser.on( "-n", "--new", FalseClass,
496
- "Create a new entry instead of editing an existing one." ) do
497
- options.newentry = true
498
- end
499
-
500
- end
501
-
502
-
503
- ### Convert the given +patterns+ to branchsets relative to the current branch and return
504
- ### them. This is used to map shell arguments like 'cn=*', 'Hosts', 'cn=dav*' into
505
- ### branchsets that will find matching entries.
506
- def convert_to_branchsets( *patterns )
507
- self.log.debug "Turning %d patterns into branchsets." % [ patterns.length ]
508
- return patterns.collect do |pat|
509
- key, val = pat.split( /\s*=\s*/, 2 )
510
- self.log.debug " making a filter out of %p => %p" % [ key, val ]
511
- @currbranch.filter( key => val )
512
- end
513
- end
514
-
515
-
516
- ### Remove the entry specified by +rdn+.
517
- def rm_command( options, *rdns )
518
- branchsets = self.convert_to_branchsets( *rdns )
519
- coll = Treequel::BranchCollection.new( *branchsets )
520
-
521
- branches = coll.all
522
-
523
- msg = "About to delete the following entries:\n" +
524
- branches.collect {|br| " #{br.dn}" }.join("\n")
525
-
526
- ask_for_confirmation( msg ) do
527
- branches.each do |branch|
528
- branch.delete
529
- message "Deleted #{branch.dn}."
530
- end
531
- end
532
- end
533
- set_options :rm do |oparser, options|
534
- oparser.banner = "rm [DNs]"
535
- end
536
-
537
-
538
- ### Find entries that match the given filter_clauses.
539
- def grep_command( options, *filter_clauses )
540
- branchset = filter_clauses.inject( @currbranch ) do |branch, clause|
541
- branch.filter( clause )
542
- end
543
-
544
- message "Searching for entries that match '#{branchset.to_s}'"
545
-
546
- entries = branchset.all
547
- output = columnize( entries ).
548
- collect do |row|
549
- row.gsub( /#{ATTRIBUTE_TYPE}=\s*\S+/ ) do |rdn|
550
- format_rdn( rdn )
551
- end
552
- end
553
- message( output )
554
- end
555
- set_options :grep do |oparser, options|
556
- oparser.banner = "grep [OPTIONS] FILTER"
557
- end
558
-
559
-
560
- ### Bind as a user.
561
- def bind_command( options, *args )
562
- binddn = (args.first || prompt( "Bind DN/UID" )) or
563
- raise "Cancelled."
564
- password = prompt_for_password()
565
-
566
- # Try to turn a non-DN into a DN
567
- user = nil
568
- if binddn.index( '=' )
569
- user = Treequel::Branch.new( @dir, binddn )
570
- else
571
- user = @dir.filter( :uid => binddn ).first
572
- end
573
- raise "No user found for %p" % [ binddn ] unless user.exists?
574
-
575
- @dir.bind( user, password )
576
- message "Bound as #{user}"
577
- end
578
- set_options :bind do |oparser, options|
579
- oparser.banner = "bind [BIND_DN or UID]"
580
- oparser.separator "If you don't specify a BIND_DN, you will be prompted for it."
581
- end
582
-
583
-
584
- ### Handle a command from the user that doesn't exist.
585
- def handle_missing_cmd( *args )
586
- command = args.shift || '(testing?)'
587
- message "Unknown command %p" % [ command ]
588
- message "Known commands: ", ' ' + @commands.join(', ')
589
- end
590
-
591
-
592
- ### Find methods that implement commands and return them in a sorted Array.
593
- def find_commands
594
- return self.methods.
595
- collect {|mname| mname.to_s }.
596
- grep( /^(\w+)_command$/ ).
597
- collect {|mname| mname[/^(\w+)_command$/, 1] }.
598
- sort
599
- end
600
-
601
-
602
- #######
603
- private
604
- #######
605
-
606
- ### Dump the specified +object+ to a file as YAML, invoke an editor on it, then undump the
607
- ### result. If the file has changed, return the updated object, else returns +nil+.
608
- def edit_in_yaml( object )
609
- yaml = object.to_yaml.gsub( /^\s*$/, '' )
610
- filename = Digest::SHA1.hexdigest( yaml )
611
- tempfile = Tempfile.new( filename )
612
-
613
- message "Object as YAML is: ", yaml
614
- tempfile.print( yaml )
615
- tempfile.close
616
-
617
- new_yaml = edit( tempfile.path )
618
-
619
- if new_yaml == yaml
620
- message "Unchanged."
621
- return nil
622
- else
623
- return YAML.load( new_yaml )
624
- end
625
- end
626
-
627
-
628
- ### Create a command table that maps command abbreviations to the Method object that
629
- ### implements it.
630
- def make_command_table( commands )
631
- table = commands.abbrev
632
- table.keys.each do |abbrev|
633
- mname = table.delete( abbrev )
634
- table[ abbrev ] = self.method( mname + '_command' )
635
- end
636
-
637
- return table
638
- end
639
-
640
-
641
- ### Return the specified args as a string, quoting any that have a space.
642
- def quotelist( *args )
643
- return args.flatten.collect {|part| part =~ /\s/ ? part.inspect : part}
644
- end
645
-
646
-
647
- ### Run the specified command +cmd+ with system(), failing if the execution
648
- ### fails.
649
- def run_command( *cmd )
650
- cmd.flatten!
651
-
652
- if cmd.length > 1
653
- self.log.debug( quotelist(*cmd) )
654
- else
655
- self.log.debug( cmd )
656
- end
657
-
658
- if $dryrun
659
- self.log.error "(dry run mode)"
660
- else
661
- system( *cmd )
662
- unless $?.success?
663
- raise "Command failed: [%s]" % [cmd.join(' ')]
664
- end
665
- end
666
- end
667
-
668
-
669
- ### Run the given +cmd+ with the specified +args+ without interpolation by the shell and
670
- ### return anything written to its STDOUT.
671
- def read_command_output( cmd, *args )
672
- self.log.debug "Reading output from: %s" % [ cmd, quotelist(cmd, *args) ]
673
- output = IO.read( '|-' ) or exec cmd, *args
674
- return output
675
- end
676
-
677
-
678
- ### Run a subordinate Rake process with the same options and the specified +targets+.
679
- def rake( *targets )
680
- opts = ARGV.select {|arg| arg[0,1] == '-' }
681
- args = opts + targets.map {|t| t.to_s }
682
- run 'rake', '-N', *args
683
- end
684
-
685
-
686
- ### Open a pipe to a process running the given +cmd+ and call the given block with it.
687
- def pipeto( *cmd )
688
- $DEBUG = true
689
-
690
- cmd.flatten!
691
- self.log.info( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
692
- if $dryrun
693
- message "(dry run mode)"
694
- else
695
- open( '|-', 'w+' ) do |io|
696
-
697
- # Parent
698
- if io
699
- yield( io )
700
-
701
- # Child
702
- else
703
- exec( *cmd )
704
- raise "Command failed: [%s]" % [cmd.join(' ')]
705
- end
706
- end
707
- end
708
- end
709
-
710
-
711
- ### Return the fully-qualified path to the specified +program+ in the PATH.
712
- def which( program )
713
- ENV['PATH'].split(/:/).
714
- collect {|dir| Pathname.new(dir) + program }.
715
- find {|path| path.exist? && path.executable? }
716
- end
717
-
718
-
719
- ### Output the specified message +parts+.
720
- def message( *parts )
721
- $stderr.puts( *parts )
722
- end
723
-
724
-
725
- ### Output the specified <tt>msg</tt> as an ANSI-colored error message
726
- ### (white on red).
727
- def error_message( msg, details='' )
728
- $stderr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + ' ' + details
729
- end
730
- alias :error :error_message
731
-
732
-
733
- ### Highlight and embed a prompt control character in the given +string+ and return it.
734
- def make_prompt_string( string )
735
- return CLEAR_CURRENT_LINE + colorize( 'bold', 'yellow' ) { string + ' ' }
736
- end
737
-
738
-
739
- ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
740
- ### return the user's input with leading and trailing spaces removed. If a
741
- ### test is provided, the prompt will repeat until the test returns true.
742
- ### An optional failure message can also be passed in.
743
- def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
744
- prompt_string.chomp!
745
- prompt_string << ":" unless /\W$/.match( prompt_string )
746
- response = nil
747
-
748
- begin
749
- prompt = make_prompt_string( prompt_string )
750
- response = readline( prompt ) || ''
751
- response.strip!
752
- if block_given? && ! yield( response )
753
- error_message( failure_msg + "\n\n" )
754
- response = nil
755
- end
756
- end while response.nil?
757
-
758
- return response
759
- end
760
-
761
-
762
- ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
763
- ### substituting the given <tt>default</tt> if the user doesn't input
764
- ### anything. If a test is provided, the prompt will repeat until the test
765
- ### returns true. An optional failure message can also be passed in.
766
- def prompt_with_default( prompt_string, default, failure_msg="Try again." )
767
- response = nil
768
-
769
- begin
770
- default ||= '~'
771
- response = prompt( "%s [%s]" % [ prompt_string, default ] )
772
- response = default.to_s if !response.nil? && response.empty?
773
-
774
- self.log.debug "Validating response %p" % [ response ]
775
-
776
- # the block is a validator. We need to make sure that the user didn't
777
- # enter '~', because if they did, it's nil and we should move on. If
778
- # they didn't, then call the block.
779
- if block_given? && response != '~' && ! yield( response )
780
- error_message( failure_msg + "\n\n" )
781
- response = nil
782
- end
783
- end while response.nil?
784
-
785
- return nil if response == '~'
786
- return response
787
- end
788
-
789
-
790
- ### Prompt for an array of values
791
- def prompt_for_multiple_values( label, default=nil )
792
- message( MULTILINE_PROMPT % [label] )
793
- if default
794
- message "Enter a single blank line to keep the default:\n %p" % [ default ]
795
- end
796
-
797
- results = []
798
- result = nil
799
-
800
- begin
801
- result = readline( make_prompt_string("> ") )
802
- if result.nil? || result.empty?
803
- results << default if default && results.empty?
804
- else
805
- results << result
806
- end
807
- end until result.nil? || result.empty?
808
-
809
- return results.flatten
810
- end
811
-
812
-
813
- ### Turn echo and masking of input on/off.
814
- def noecho( masked=false )
815
- rval = nil
816
- term = Termios.getattr( $stdin )
817
-
818
- begin
819
- newt = term.dup
820
- newt.c_lflag &= ~Termios::ECHO
821
- newt.c_lflag &= ~Termios::ICANON if masked
822
-
823
- Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
824
-
825
- rval = yield
826
- ensure
827
- Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
828
- end
829
-
830
- return rval
831
- end
832
-
833
-
834
- ### Prompt the user for her password, turning off echo if the 'termios' module is
835
- ### available.
836
- def prompt_for_password( prompt="Password: " )
837
- rval = nil
838
- noecho( true ) do
839
- $stderr.print( prompt )
840
- rval = ($stdin.gets || '').chomp
841
- end
842
- $stderr.puts
843
- return rval
844
- end
845
-
846
-
847
- ### Display a description of a potentially-dangerous task, and prompt
848
- ### for confirmation. If the user answers with anything that begins
849
- ### with 'y', yield to the block. If +abort_on_decline+ is +true+,
850
- ### any non-'y' answer will fail with an error message.
851
- def ask_for_confirmation( description, abort_on_decline=true )
852
- puts description
853
-
854
- answer = prompt_with_default( "Continue?", 'n' ) do |input|
855
- input =~ /^[yn]/i
856
- end
857
-
858
- if answer =~ /^y/i
859
- return yield
860
- elsif abort_on_decline
861
- error "Aborted."
862
- fail
863
- end
864
-
865
- return false
866
- end
867
- alias :prompt_for_confirmation :ask_for_confirmation
868
-
869
-
870
- ### Search line-by-line in the specified +file+ for the given +regexp+, returning the
871
- ### first match, or nil if no match was found. If the +regexp+ has any capture groups,
872
- ### those will be returned in an Array, else the whole matching line is returned.
873
- def find_pattern_in_file( regexp, file )
874
- rval = nil
875
-
876
- File.open( file, 'r' ).each do |line|
877
- if (( match = regexp.match(line) ))
878
- rval = match.captures.empty? ? match[0] : match.captures
879
- break
880
- end
881
- end
882
-
883
- return rval
884
- end
885
-
886
-
887
- ### Search line-by-line in the output of the specified +cmd+ for the given +regexp+,
888
- ### returning the first match, or nil if no match was found. If the +regexp+ has any
889
- ### capture groups, those will be returned in an Array, else the whole matching line
890
- ### is returned.
891
- def find_pattern_in_pipe( regexp, *cmd )
892
- output = []
893
-
894
- self.log.info( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
895
- Open3.popen3( *cmd ) do |stdin, stdout, stderr|
896
- stdin.close
897
-
898
- output << stdout.gets until stdout.eof?
899
- output << stderr.gets until stderr.eof?
900
- end
901
-
902
- result = output.find { |line| regexp.match(line) }
903
- return $1 || result
904
- end
905
-
906
-
907
- ### Invoke the user's editor on the given +filename+ and return the exit code
908
- ### from doing so.
909
- def edit( filename )
910
- editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
911
- system editor, filename.to_s
912
- unless $?.success? || editor =~ /vim/i
913
- raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
914
- end
915
- return File.read( filename )
916
- end
917
-
918
-
919
- ### Make an easily-comparable version vector out of +ver+ and return it.
920
- def vvec( ver )
921
- return ver.split('.').collect {|char| char.to_i }.pack('N*')
922
- end
923
-
924
-
925
- ### Return an ANSI-colored version of the given +rdn+ string.
926
- def format_rdn( rdn )
927
- rdn.split( /,/ ).collect do |rdn|
928
- key, val = rdn.split( /\s*=\s*/, 2 )
929
- colorize( :white ) { key } +
930
- colorize( :bold, :black ) { '=' } +
931
- colorize( :bold, :white ) { val }
932
- end.join( colorize(',', :green) )
933
- end
934
-
935
-
936
- ### Highlight LDIF and return it.
937
- def format_ldif( ldif )
938
- return ldif.gsub( /^(\S[^:]*)(::?)\s*(.*)$/ ) do
939
- key, sep, val = $1, $2, $3
940
- case sep
941
- when '::'
942
- colorize( :cyan ) { key } + ':: ' + colorize( :dark, :white ) { val }
943
- when ':'
944
- colorize( :bold, :cyan ) { key } + ': ' + colorize( :dark, :white ) { val }
945
- else
946
- key + sep + ' ' + val
947
- end
948
- end
949
- end
950
-
951
-
952
- ### Return the specified +entries+ as an Array of span-sorted columns fit to the
953
- ### current terminal width.
954
- def columnize( *entries )
955
- return Columnize.columnize( entries.flatten, @columns, ' ' )
956
- end
957
-
958
- end # class Treequel::Shell
959
-
960
-
961
- ldapuri = URI( ARGV.shift || 'ldap://localhost' )
962
- Treequel::Shell.new( ldapuri ).run
963
-