2
# vim: set nosta noet ts=4 sw=4:
4
# Copyright (c) 2006-2013, Mahlon E. Smith <mahlon@martini.nu>
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
9
# * Redistributions of source code must retain the above copyright
10
# notice, this list of conditions and the following disclaimer.
12
# * Redistributions in binary form must reproduce the above copyright
13
# notice, this list of conditions and the following disclaimer in the
14
# documentation and/or other materials provided with the distribution.
16
# * Neither the name of Mahlon E. Smith nor the names of his
17
# contributors may be used to endorse or promote products derived
18
# from this software without specific prior written permission.
20
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
21
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
24
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
Shelldap - A program for interacting with an LDAP server via a shell-like interface
37
Shelldap /LDAP::Shell is a program for interacting with an LDAP server via a shell-like
40
This is not meant to be an exhaustive LDAP editing and browsing
41
interface, but rather an intuitive shell for performing basic LDAP
42
tasks quickly and with minimal effort.
46
shelldap --server example.net [--help]
50
- Upon successful authenticated binding, credential information is
51
auto-cached to ~/.shelldap.rc -- future loads require no command line
54
- Custom 'description maps' for entry listings. (See the 'list' command.)
56
- History and autocomplete via readline, if installed.
58
- Automatic reconnection attempts if the connection is lost with the
61
- Basic schema introspection for quick reference.
63
- It feels like a semi-crippled shell, making LDAP browsing and editing
64
at least halfway pleasurable.
68
All command line options follow getopts long conventions.
70
shelldap --server example.net --basedn dc=your,o=company
72
You may also optionally create a ~/.shelldap.rc file with command line
73
defaults. This file should be valid YAML. (This file is generated
74
automatically on a successful bind auth.)
78
server: ldap.example.net
79
binddn: cn=Manager,dc=your,o=company
81
basedn: dc=your,o=company
83
tls_cacert: /etc/ssl/certs/cacert.pem
84
tls_cert: ~/.ssl/client.cert.pem
85
tls_key: ~/.ssl/private/client.key.pem
91
Optional. Use an alternate configuration file, instead of the
92
default ~/.shelldap.rc.
94
--configfile /tmp/alternate-config.yml
95
-f /tmp/alternate-config.yml
97
This config file overrides values found in the default config, so
98
you can easily have separate config files for connecting to your
99
cn=monitor or cn=log overlays (for example.)
107
Required. The LDAP server to connect to. This can be a hostname, IP
110
--server ldaps://ldap.example.net
111
-H ldaps://ldap.example.net
119
The full dn of a user to authenticate as. If not specified, defaults to
120
an anonymous bind. You will be prompted for a password.
122
--binddn cn=Manager,dc=your,o=company
123
-D cn=Manager,dc=your,o=company
131
The directory 'root' of your LDAP server. If omitted, shelldap will
132
try and ask the server for a sane default.
134
--basedn dc=your,o=company
143
Force password prompting. Useful to temporarily override cached
150
A space separated list of SASL mechanisms. Requires the Authen::SASL
153
--sasl "PLAIN CRAM-MD5 GSSAPI"
161
Enables TLS over what would normally be an insecure connection.
162
Requires server side support.
166
Specify CA Certificate to trust.
168
--tls_cacert /etc/ssl/certs/cacert.pem
172
The TLS client certificate.
174
--tls_cert ~/.ssl/client.cert.pem
178
The TLS client key. Not specifying a key will connect via TLS without
181
--tls_key ~/.ssl/private/client.key.pem
189
Set the time to cache directory lookups in seconds.
191
By default, directory lookups are cached for 300 seconds, to speed
192
autocomplete up when changing between different basedns.
194
Modifications to the directory automatically reset the cache. Directory
195
listings are not cached. (This is just used for autocomplete.) Set it
196
to 0 to disable caching completely.
204
Set the maximum time an LDAP operation can take before it is cancelled.
212
Print extra operational info out, and backtrace on fatal error.
220
Display the version number.
224
=head1 SHELL COMMANDS
230
Display an LDIF dump of an entry. Globbing is supported. Specify
231
either the full dn, or an rdn. For most commands, rdns are local to the
232
current search base. ('cwd', as translated to shell speak.) You may additionally
233
add a list of attributes to display. Use '+' for server side attributes.
237
cat uid=mahlon,ou=People,dc=example,o=company
238
cat uid=mahlon + userPassword
242
Change directory. Translated to LDAP, this changes the current basedn.
243
All commands after a 'cd' operate within the new basedn.
245
cd change to 'home' basedn
246
cd ~ change to the binddn, or basedn if anonymously bound
247
cd - change to previous node
248
cd ou=People change to explicit path below current node
249
cd .. change to parent node
250
cd ../../ou=Groups change to node ou=Groups, which is a sibling
251
to the current node's grandparent
253
Since LDAP doesn't actually limit what can be a container object, you
254
can actually cd into any entry. Many commands then work on '.', meaning
255
"wherever I currently am."
266
Copy an entry to a different dn path. All copies are relative to the
267
current basedn, unless a full dn is specified. All attributes are
268
copied, then an LDAP moddn() is performed.
270
copy uid=mahlon uid=bob
271
copy uid=mahlon ou=Others,dc=example,o=company
272
copy uid=mahlon,ou=People,dc=example,o=company uid=mahlon,ou=Others,dc=example,o=company
278
Create an entry from scratch. Arguments are space separated objectClass
279
names. Possible objectClasses are derived automatically from the
280
server, and will tab-complete.
282
After the classes are specified, an editor will launch. Required
283
attributes are listed first, then optional attributes. Optionals are
284
commented out. After the editor exits, the resulting LDIF is validated
285
and added to the LDAP directory.
287
create top person organizationalPerson inetOrgPerson posixAccount
293
Remove an entry from the directory. Globbing is supported.
294
All deletes are sanity-prompted.
303
Edit an entry in an external editor. After the editor exits, the
304
resulting LDIF is sanity checked, and changes are written to the LDAP
313
Show values for various runtime variables.
317
Search for arbitrary LDAP filters, and return matching dn results.
318
The search string must be a valid LDAP filter.
321
grep uid=mahlon ou=People
322
grep -r (&(uid=mahlon)(objectClass=*))
328
View schema information about a given entry, or a list of arbitrary
329
objectClasses, along with the most common flags for the objectClass
333
inspect posixAccount organizationalUnit
336
The output is a list of found objectClasses, their schema heirarchy
337
(up to 'top'), whether or not they are a structural class, and then
338
a merged list of all valid attributes for the given objectClasses.
339
Attributes are marked as either required or optional, and whether
340
they allow multiple values or not.
342
If you ask for the special "_schema" object, the raw server schema
347
List entries for the current basedn. Globbing is supported.
355
In 'long' mode, descriptions are listed as well, if they exist.
356
There are some default 'long listing' mappings for common objectClass
357
types. You can additionally specify your own mappings in your
358
.shelldap.rc, like so:
362
objectClass: attributename
364
posixGroup: gidNumber
369
Creates a new 'organizationalUnit' entry.
376
Move an entry to a different dn path. Usage is identical to B<copy>.
382
If supported server side, change the password for a specified entry.
383
The entry must have a 'userPassword' attribute.
389
Print the 'working directory' - aka, the current ldap basedn.
393
Modify various runtime variables normally set from the command line.
400
Show current auth credentials. Unless you specified a binddn, this
401
will just show an anonymous bind.
407
Referral support. Currently, if you try to write to a replicant slave,
408
you'll just get a referral. It would be nice if shelldap automatically
411
For now, it only makes sense to connect to a master if you plan on doing
414
=head1 BUGS / LIMITATIONS
416
There is no support for editing binary data. If you need to edit base64
417
stuff, just feed it to the regular ldapmodify/ldapadd/etc tools.
421
Mahlon E. Smith <mahlon@martini.nu>
434
LDAP_OPERATIONS_ERROR
435
LDAP_TIMELIMIT_EXCEEDED
441
LDAP_CONNECT_ERROR /;
442
use Net::LDAP::Util qw/ canonical_dn ldap_explode_dn /;
448
use base 'Term::Shell';
449
require Net::LDAP::Extension::SetPassword;
451
my $conf = $main::conf;
453
# make 'die' backtrace in debug mode
454
$SIG{'__DIE__'} = \&Carp::confess if $conf->{'debug'};
457
########################################################################
458
### U T I L I T Y F U N C T I O N S
459
########################################################################
461
### Initial shell behaviors.
466
$self->{'API'}->{'match_uniq'} = 0;
468
$self->{'editor'} = $conf->{'editor'} || $ENV{'EDITOR'} || 'vi';
469
$self->{'env'} = [ qw/ debug cacheage timeout / ];
471
# let autocomplete work with the '=' character
472
my $term = $self->term();
473
$term->Attribs->{'basic_word_break_characters'} =~ s/=//m;
474
$term->Attribs->{'completer_word_break_characters'} =~ s/=//m;
478
$term->history_truncate_file("$ENV{'HOME'}/.shelldap_history", 50);
479
$term->ReadHistory("$ENV{'HOME'}/.shelldap_history");
482
# gather metadata from the LDAP server
483
$self->{'root_dse'} = $self->ldap->root_dse() or
484
die "Unable to retrieve LDAP server information. (Doublecheck connection arguments.)\n";
485
$self->{'schema'} = $self->ldap->schema();
487
# get an initial list of all objectClasses
488
$self->{'objectclasses'} = [];
489
foreach my $o ( $self->{'schema'}->all_objectclasses() ) {
490
push @{ $self->{'objectclasses'} }, $o->{'name'};
493
if ( $conf->{'debug'} ) {
494
my @versions = $self->{'root_dse'}->get_value('supportedLDAPVersion');
495
print "Connected to $conf->{'server'}\n";
496
print "Supported LDAP version: ", ( join ', ', @versions ), "\n";
497
print "Cipher in use: ", $self->ldap()->cipher(), "\n";
500
# try an initial search and bail early if it doesn't work. (bad baseDN?)
501
my $s = $self->search();
502
die "LDAP baseDN error: ", $s->{'message'}, "\n" if $s->{'code'};
504
# okay, now do an initial population of 'cwd' for autocomplete.
505
$self->update_entries();
507
# whew, okay. Update prompt, wait for input!
508
$self->update_prompt();
514
### Return an LDAP connection handle, creating it if necessary.
521
# use cached connection object if it exists
522
return $self->{'ldap'} if $self->{'ldap'};
524
# fill in potentially missing info
525
die "No server specified.\n" unless $conf->{'server'};
527
# Emit a nicer error message if IO::Socket::SSL is
528
# not installed and Net::LDAP decides it is required.
530
if ( $conf->{'tls'} || $conf->{'server'} =~ m|ldaps://| ) {
531
eval 'use IO::Socket::SSL';
532
die qq{IO::Socket::SSL not installed, but is required for SSL or TLS connections.
533
You may try connecting insecurely, or install the module and try again.\n} if $@;
536
# Prompt for a password after disabling local echo.
538
if ( ($conf->{'binddn'} && ! $conf->{'bindpass'}) || $conf->{'promptpass'} ) {
539
print "Bind password: ";
540
Term::ReadKey::ReadMode 2;
541
chomp( $conf->{'bindpass'} = <STDIN> );
542
Term::ReadKey::ReadMode 0;
546
# make the connection
547
my $ldap = Net::LDAP->new( $conf->{'server'} )
548
or die "Unable to connect to LDAP server '$conf->{'server'}': $!\n";
550
# secure connection options
552
if ( $conf->{'tls'} ) {
553
if ( $conf->{'tls_key'} ) {
556
cafile => $conf->{'tls_cacert'},
557
clientcert => $conf->{'tls_cert'},
558
clientkey => $conf->{'tls_key'},
560
print "Key Passphrase: ";
561
Term::ReadKey::ReadMode 2;
562
chomp( my $secret = <STDIN> );
563
Term::ReadKey::ReadMode 0;
569
$ldap->start_tls( verify => 'none' );
573
eval 'use Authen::SASL';
574
my ( $sasl, $sasl_conn );
575
my $has_sasl = ! defined( $@ );
577
if ( $has_sasl && $conf->{'sasl'} ) {
578
my $serv = $conf->{'server'};
579
$serv =~ s!^ldap[si]?://!!;
580
$sasl = Authen::SASL->new( mechanism => $conf->{'sasl'} );
581
$sasl_conn = $sasl->client_new('ldap', $serv);
586
if ( $has_sasl && $sasl_conn ) {
589
password => $conf->{'bindpass'},
594
# simple bind as an authenticated dn
596
elsif ( $conf->{'binddn'} ) {
599
password => $conf->{'bindpass'}
606
$rv = $ldap->bind(sasl => $sasl_conn);
609
my $err = $rv->error();
612
( $conf->{'binddn'} ? $conf->{'binddn'} : 'anonymous' ) .
613
" to " . $conf->{'server'} . ": $err\n"
617
$err .= " (try the --tls flag?)" if $err =~ /confidentiality required/i;
618
$err .= "\n" . $sasl->error() if $sasl;
619
die "LDAP bind error: $err\n";
622
# Offer to cache authentication info.
623
# If we enter this conditional, we have successfully authed with the server
624
# (non anonymous), and we haven't cached anything in the past.
626
if ( $conf->{'binddn'} && ! -e $conf->{'configfile'} ) {
627
print "Would you like to cache your connection information? [Yn]: ";
628
chomp( my $response = <STDIN> );
629
unless ( $response =~ /^n/i ) {
630
YAML::Syck::DumpFile( $conf->{'configfile'}, $conf );
631
chmod 0600, $conf->{'configfile'};
632
print "Connection info cached to $conf->{'configfile'}.\n";
636
$self->{'ldap'} = $ldap;
641
### Return a new LDIF object, suitable for populating with
642
### a Net::LDAP::Entry.
647
my $use_temp = shift;
649
# create tmpfile and link ldif object with it
652
my ( undef, $fname ) =
653
File::Temp::tempfile( 'shelldap_XXXXXXXX', SUFFIX => '.ldif', DIR => '/tmp', UNLINK => 1 );
654
$self->{'ldif'} = Net::LDAP::LDIF->new( $fname, 'w', sort => 1, wrap => $self->wrapsize );
655
$self->{'ldif_fname'} = $fname;
660
$self->{'ldif'} = Net::LDAP::LDIF->new( \*STDOUT, 'w', sort => 1, wrap => $self->wrapsize );
663
return $self->{'ldif'};
667
### Return an Entry object from an LDIF filename, or undef if there was an error.
672
my $ldif = Net::LDAP::LDIF->new( shift(), 'r' );
677
eval { $e = $ldif->read_entry(); };
684
### Given a filename, return an md5 checksum.
689
my $file = shift or return;
691
my $md5 = Digest::MD5->new();
692
open F, $file or die "Unable to read file: $!\n";
693
my $hash = $md5->addfile( *F )->hexdigest();
700
### Find and return the current terminal width.
706
my $wrap = $conf->{'wrap'};
709
my $term = Term::ReadLine->new( 1 );
710
( $rows, $wrap ) = $term->get_screen_size() unless $wrap;
718
### Used by Term::Shell to generate the prompt.
723
return $self->{'prompt'};
727
### Display the current working entry as the prompt,
728
### truncating if necessary.
733
my $base = $self->base();
735
if ( length $base > 50 ) {
736
my $cwd_dn = $1 if $base =~ /^(.*?),/;
737
$self->{'prompt'} = "... $cwd_dn > ";
741
$prompt =~ s/$conf->{'basedn'}/~/;
742
$self->{'prompt'} = "$prompt > ";
748
### Prompt the user to re-edit their LDIF on error.
749
### Returns true if the user wants to do so.
751
sub prompt_edit_again
754
print "Edit again? [Yn]: ";
755
chomp( my $ans = <STDIN> );
756
return $ans !~ /^n/i;
760
### Return the basedn of the LDAP connection, being either explicitly
761
### configured or determined automatically from server metadata.
766
$self->{'base'} ||= $conf->{'basedn'};
768
# try and determine base automatically from rootDSE
770
unless ( $self->{'base'} ) {
771
my @namingContexts = $self->{'root_dse'}->get_value('namingContexts');
772
$conf->{'basedn'} = $namingContexts[0];
773
$self->{'base'} = $namingContexts[0];
776
my $base = canonical_dn( $_[0], casefold => 'none' );
777
$self->{'base'} = $base if $base;
779
return $self->{'base'};
783
### Returns true if the specified dn is valid on this LDAP server.
788
my $dn = shift or return 0;
790
my $r = $self->search({ base => $dn });
791
return $r->{'code'} == LDAP_SUCCESS ? 1 : 0;
795
### Perform an LDAP search.
797
### Returns a hashref containing the return code and
798
### an arrayref of Net::LDAP::Entry objects.
803
my $opts = shift || {};
805
$opts->{'base'} ||= $self->base(),
806
$opts->{'filter'} ||= '(objectClass=*)';
807
$opts->{'scope'} ||= 'base';
810
return $self->ldap->search(
811
base => $opts->{'base'},
812
filter => $opts->{'filter'},
813
scope => $opts->{'scope'},
814
timelimit => $conf->{'timeout'},
815
typesonly => ! $opts->{'vals'},
816
attrs => $opts->{'attrs'} || ['*']
820
my $s = $self->with_retry( $search );
823
message => $s->error(),
828
$opts->{'scope'} eq 'base' ? [ $s->shift_entry() ] : [ $s->entries() ];
834
### Maintain the cache of possible autocomplete values for
841
my $base = lc( $self->base() );
843
my $s = $opts{'search'} || $self->search({ scope => 'one', base => $base });
845
$self->{'cwd_entries'} = [];
846
return if $s->{'code'};
849
$self->{'cache'} ||= {};
850
$self->{'cache'}->{ $base } ||= {};
851
$self->{'cache'}->{ $base } = {} if $opts{'clearcache'};
852
my $cache = $self->{'cache'}->{ $base };
855
if ( ! exists $cache->{'entries'}
856
or $now - $cache->{'timestamp'} > $conf->{'cacheage'} )
858
$self->debug("Caching entries for $base\n");
859
foreach my $e ( @{ $s->{'entries'} } ) {
862
$rdn =~ s/,$base//i; # remove base from display
863
push @{ $self->{'cwd_entries'} }, $rdn;
865
$cache->{'timestamp'} = $now;
866
$cache->{'entries'} = $self->{'cwd_entries'};
869
$self->debug("Using cached lookups for $base\n");
872
$self->{'cwd_entries'} = $cache->{'entries'};
877
### Roughly convert a given path to a DN.
879
### Additionally support:
885
### Synopsis: $dn = $self->path_to_dn( $path );
892
my $curbase = $self->base();
894
# support empty 'cd' or 'cd ~' going to root
895
return $conf->{'basedn'} if ! $path || $path eq '~';
897
# return current base DN
898
return $curbase if $path eq '.';
901
return $self->{'previous_base'} if $path eq '-';
903
# relative path, upwards
905
if ( $path =~ /^\.\./o ) {
906
# support '..' (possibly iterated and as prefix to a DN)
907
my @base = @{ ldap_explode_dn($curbase, casefold => 'none') };
909
# deal with leading ..,
911
while ( $path =~ /^\.\./ ) {
912
shift( @base ) if @base;
914
last if $path !~ /[,\/]\s*/;
915
$path =~ s/[,\/]\s*//;
918
# append the new dn to the node if one was specified:
919
# cd ../../cn=somewhere vs
922
my $newbase_root = canonical_dn( \@base, casefold => 'none' );
923
$path = $path ? $path . ',' . $newbase_root : $newbase_root;
926
# attach the base if it isn't already there (this takes care of
927
# deeper relative nodes and absolutes)
930
$path = "$path," . $curbase unless $path =~ /$curbase/;
937
### Given an array ref of shell-like globs,
938
### create and return a Net::LDAP::Filter object.
943
my $globs = shift or return;
945
return unless ref $globs eq 'ARRAY';
946
return unless scalar @$globs;
949
$filter = join('', map { (/^\(.*\)$/o) ? $_ : "($_)" } @$globs);
950
$filter = '(|' . $filter . ')' if (scalar(@$globs) > 1);
951
$filter = Net::LDAP::Filter->new( $filter );
954
$self->debug( 'Filter parsed as: ' . $filter->as_string() . "\n" );
957
print "Error parsing filter.\n";
965
### Given an arrayref of objectClasses, pull a complete list of
966
### required and optional attrbutes. Returns two arrayrefs.
971
my $ocs = shift or return [], [];
973
my ( %seen, @must_attr, @may_attr );
974
foreach my $oc ( sort @{$ocs} ) {
977
my @must = $self->{'schema'}->must( $oc );
978
foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @must ) {
979
next if $attr->{'name'} =~ /^objectclass$/i;
980
next if $seen{ $attr->{'name'} };
981
push @must_attr, $attr->{'name'};
982
$seen{ $attr->{'name'} }++;
986
my @may = $self->{'schema'}->may( $oc );
987
foreach my $attr ( sort { $a->{'name'} cmp $b->{'name'} } @may ) {
988
next if $attr->{'name'} =~ /^objectclass$/i;
989
next if $seen{ $attr->{'name'} };
990
push @may_attr, $attr->{'name'};
991
$seen{ $attr->{'name'} }++;
995
return \@must_attr, \@may_attr;
999
### Check whether a given string can be used directly as
1000
### an LDAP search filter.
1002
### Synopsis: $yesNo = $self->is_valid_filter($string);
1007
my $filter = shift or return;
1009
return Net::LDAP::Filter->new( $filter ) ? 1 : 0;
1013
### Call code in subref $action, if there's any connection related errors,
1014
### try it one additional time before giving up. This should take care of
1015
### most server disconnects due to timeout and other generic connection
1016
### errors, and will attempt to transparently re-establish a connection.
1023
my $rv = $action->();
1024
if ( $rv->code() == LDAP_OPERATIONS_ERROR ||
1025
$rv->code() == LDAP_TIMELIMIT_EXCEEDED ||
1026
$rv->code() == LDAP_BUSY ||
1027
$rv->code() == LDAP_UNAVAILABLE ||
1028
$rv->code() == LDAP_OTHER ||
1029
$rv->code() == LDAP_SERVER_DOWN ||
1030
$rv->code() == LDAP_TIMEOUT ||
1031
$rv->code() == LDAP_NO_MEMORY ||
1032
$rv->code() == LDAP_CONNECT_ERROR ) {
1034
$self->debug( "Error ". $rv->code() . ", retrying.\n" );
1035
$self->{'ldap'} = undef;
1043
### little. yellow. different. better.
1048
return unless $conf->{'debug'};
1056
### Autocomplete values: Returns cached children entries.
1058
sub autocomplete_cwd
1061
return @{ $self->{'cwd_entries'} };
1065
### Autocomplete values: Returns previously set shelldap environment values.
1070
return @{ $self->{'env'} };
1074
### Autocomplete values: Returns all objectClasses as defined
1075
### by the LDAP server.
1080
return @{ $self->{'objectclasses'} };
1084
### Autocomplete values: Returns all objectClasses as defined
1085
### by the LDAP server, along with current children DNs.
1090
return ('_schema', @{ $self->{'objectclasses'} }, @{ $self->{'cwd_entries'} });
1094
### Inject various autocomplete and alias routines into the symbol table.
1114
# setup autocompletes
1115
foreach ( %cmd_map ) {
1117
my $sub = "comp_$_";
1118
*$sub = \&autocomplete_cwd;
1120
*comp_touch = \&comp_create;
1121
*comp_export = \&comp_setenv;
1125
# Term::Shell has an alias_* feature, but
1126
# it seems to work about 90% of the time.
1127
# that last 10% is something of a mystery.
1129
$cmd_map{'create'} = 'touch';
1130
foreach my $cmd ( keys %cmd_map ) {
1131
next unless defined $cmd_map{$cmd};
1132
my $alias_sub = 'run_' . $cmd_map{$cmd};
1133
my $real_sub = 'run_' . $cmd;
1134
*$alias_sub = \&$real_sub;
1139
### Given an $arrayref, remove LDIF continuation wrapping in place,
1140
### effectively making each entry a single line for LCS comparisons.
1147
while ( $i < scalar(@$array) ) {
1148
if ( $array->[$i] =~ /^\s/ ) {
1149
$array->[ $i - 1 ] =~ s/\n$//;
1150
$array->[ $i ] =~ s/^\s//;
1151
splice( @$array, $i - 1, 2, $array->[$i - 1] . $array->[$i] );
1160
########################################################################
1161
### S H E L L M E T H O D S
1162
########################################################################
1164
### Don't die on a newline, just no-op.
1166
sub run_ { return; }
1169
### Term::Shell hook.
1170
### Write history for each command, print shell debug actions.
1175
my ( $handler, $cmd, $args ) = @_;
1177
my $term = $self->term();
1178
eval { $term->WriteHistory("$ENV{'HOME'}/.shelldap_history"); };
1180
$self->debug( "$$cmd (" . ( join ' ', @$args ) . "), calling '$$handler'\n" );
1185
### Display an entry as LDIF to the terminal.
1191
my @attrs = (@_) ? @_ : ('*');
1194
print "No dn provided.\n";
1199
$dn = $self->base() if $dn eq '.';
1205
$s = $self->search({
1211
elsif ( $dn =~ /\*/ ) {
1212
$s = $self->search({
1220
# absolute/relative dn
1223
$dn = $self->path_to_dn( $dn );
1224
$s = $self->search({
1231
# emit error, if any
1233
if ( $s->{'code'} ) {
1234
print $s->{'message'} . "\n";
1240
foreach my $e ( @{ $s->{'entries'} } ) {
1241
$self->ldif->write_entry( $e );
1249
### Change shelldap's idea of a current working 'directory',
1250
### by adjusting the current default basedn for all searches.
1255
my $newbase = shift;
1257
# convert given path to a DN
1258
$newbase = $self->path_to_dn( $newbase );
1260
unless ( $self->is_valid_dn( $newbase ) ) {
1261
print "No such object\n";
1266
$self->{'previous_base'} = $self->base();
1269
$self->base( $newbase );
1271
# get new 'cwd' listing
1272
my $s = $self->search({ scope => 'one', attrs => [ '1.1' ] });
1273
if ( $s->{'code'} ) {
1274
print "$s->{'message'}\n";
1277
$self->update_entries( search => $s );
1279
# reflect cwd change in prompt
1280
$self->update_prompt();
1285
### Simply clear the screen.
1295
### Fetch the source DN entry, modify it's DN data
1296
### and write it back to the directory.
1301
my ( $s_dn, $d_dn ) = @_;
1304
print "No source DN provided.\n";
1308
print "No destination DN provided.\n";
1312
# convert given source path to DN
1313
$s_dn = $self->path_to_dn( $s_dn );
1315
# sanity check source
1317
my $s = $self->search({ base => $s_dn, vals => 1 });
1318
unless ( $s->{'code'} == LDAP_SUCCESS ) {
1319
print "No such object\n";
1323
# see if we're copying the entry to a nonexistent path
1325
my ( $new_dn, $old_dn );
1326
( $d_dn, $new_dn ) = ( $1, $2 ) if $d_dn =~ /^([\-\w=]+),(.*)$/;
1327
if ( $new_dn ) { # absolute
1328
unless ( $self->is_valid_dn( $new_dn ) ) {
1329
print "Invalid destination.\n";
1334
$new_dn = $self->base();
1336
$old_dn = $1 if $s_dn =~ /^[\-\w=]+,(.*)$/;
1338
# get the source entry object
1339
my $e = ${ $s->{'entries'} }[0];
1342
# add changes in new entry instead of modifying existing
1343
$e->changetype( 'add' );
1344
$e->dn( "$d_dn,$new_dn" );
1346
# get the unique attribute from the dn for modification
1347
# perhaps there is a better way to do this...?
1349
my ( $uniqkey, $uniqval ) = ( $1, $2 )
1350
if $d_dn =~ /^([\-\.\w]+)(?:\s+)?=(?:\s+)?([\-\.\s\w]+),?/;
1351
unless ( $uniqkey && $uniqval ) {
1352
print "Unable to parse unique values from RDN.\n";
1355
$e->replace( $uniqkey => $uniqval );
1357
# update (which will actually create the new entry)
1359
my $update = sub { return $e->update($self->ldap()) };
1360
my $rv = $self->with_retry( $update );
1361
print $rv->error(), "\n";
1365
$self->{'cache'}->{ $new_dn } = {} if $new_dn;
1366
$self->{'cache'}->{ $old_dn } = {} if $old_dn;
1367
$self->update_entries( clearcache => 1 );
1372
### Create a new entry from scratch, using attributes from
1373
### what the server's schema says is available from the specified
1374
### (optional) objectClass list. Populate a new LDIF file and
1375
### present an editor to the user.
1382
# manually generate some boilerplate LDIF.
1384
unless ( $self->{'create_file'} ) {
1387
( $fh, $self->{'create_file'} ) =
1388
File::Temp::tempfile( 'shelldap_XXXXXXXX', SUFFIX => '.ldif', DIR => '/tmp', UNLINK => 1 );
1390
# first print out the dn and object classes.
1392
print $fh 'dn: ???,', $self->base(), "\n";
1393
foreach my $oc ( sort @ocs ) {
1394
print $fh "objectClass: $oc\n";
1397
# gather and print attributes for requested objectClasses
1399
my ( $must_attr, $may_attr ) = $self->fetch_attributes( \@ocs );
1400
print $fh "$_: \n" foreach @{ $must_attr };
1401
print $fh "# $_: \n" foreach @{ $may_attr };
1405
# checksum the file.
1407
my $hash_orig = $self->chksum( $self->{'create_file'} );
1408
system( $self->{'editor'}, $self->{'create_file'} ) && die "Unable to launch editor: $!\n";
1410
# detect a total lack of change
1412
if ( $hash_orig eq $self->chksum($self->{'create_file'}) ) {
1413
print "Entry not modified.\n";
1414
unlink $self->{'create_file'};
1415
$self->{'create_file'} = undef;
1421
my $ldif = Net::LDAP::LDIF->new( $self->{'create_file'}, 'r', onerror => 'warn' );
1422
my $e = $ldif->read_entry();
1424
print "Unable to parse LDIF.\n";
1425
unlink $self->{'create_file'};
1426
$self->{'create_file'} = undef;
1430
# create the new entry.
1432
$e->changetype('add');
1433
my $create = sub { return $e->update($self->ldap()) };
1434
my $rv = $self->with_retry( $create );
1435
print $rv->error(), "\n";
1437
if ( $rv->code() != LDAP_SUCCESS && $self->prompt_edit_again() ) {
1438
return $self->run_create();
1441
$self->update_entries( clearcache => 1 );
1442
unlink $self->{'create_file'};
1443
$self->{'create_file'} = undef;
1448
### Remove an entry (or entries) from the LDAP directory.
1455
unless ( scalar @DNs ) {
1456
print "No dn specified.\n";
1461
unless ( $DNs[0] eq '*' ) {
1462
$filter = $self->make_filter( \@DNs ) or return;
1465
my $s = $self->search({ scope => 'one', filter => $filter });
1466
unless ( $s->{'code'} == LDAP_SUCCESS ) {
1467
print "$s->{'message'}\n";
1471
print "Are you sure? [Ny]: ";
1472
chomp( my $resp = <STDIN> );
1473
return unless $resp =~ /^y/i;
1475
foreach my $e ( @{ $s->{'entries'} } ) {
1477
my $rv = $self->ldap->delete( $dn );
1478
print "$dn: ", $rv->error(), "\n";
1481
$self->update_entries( clearcache => 1 );
1486
### Fetch an entry from the directory, write it out to disk
1487
### as LDIF, launch an editor, then compare changes and write
1488
### it back to the directory.
1496
print "No dn provided.\n";
1500
# convert given path to DN
1501
$dn = $self->path_to_dn( $dn );
1505
my $s = $self->search({ base => $dn, vals => 1 });
1506
unless ( $s->{'code'} == LDAP_SUCCESS ) {
1507
print $s->{'message'} . "\n";
1512
my $e = ${ $s->{'entries'} }[0];
1513
$e->changetype( 'modify' );
1515
# write it out to disk.
1517
unless( $self->{'edit_again'} ) {
1518
my $ldif = $self->ldif(1);
1519
$ldif->write_entry( $e );
1520
$ldif->done(); # force sync
1523
# load it into an array for potential comparison
1524
open LDIF, "$self->{'ldif_fname'}" or return;
1525
my @orig_ldif = <LDIF>;
1528
# append optional, unused attributes as comments for fast reference.
1530
unless ( $self->{'edit_again'} ) {
1531
my %current_attrs = map { $_ => 1 } $e->attributes();
1532
my ( $must_attr, $may_attr ) = $self->fetch_attributes( $e->get_value('objectClass', asref => 1) );
1534
open LDIF, ">> $self->{'ldif_fname'}";
1535
foreach my $opt_attr ( sort { $a cmp $b } @{$may_attr} ) {
1536
next if $current_attrs{ $opt_attr };
1537
print LDIF "# " . $opt_attr . ":\n";
1542
# checksum it, then open it in an editor
1544
my $hash_orig = $self->chksum( $self->{'ldif_fname'} );
1545
system( $self->{'editor'}, $self->{'ldif_fname'} ) &&
1546
die "Unable to launch editor: $!\n";
1548
# detect a total lack of change
1550
if ( $hash_orig eq $self->chksum($self->{'ldif_fname'}) ) {
1551
print "Entry not modified.\n";
1552
unlink $self->{'ldif_fname'};
1553
$self->{'edit_again'} = undef;
1557
# check changes for basic LDIF validity
1559
while( ! $self->load_ldif($self->{'ldif_fname'}) ) {
1560
print "Unable to parse LDIF.\n";
1561
if ( $self->prompt_edit_again() ) {
1562
system( $self->{'editor'}, $self->{'ldif_fname'} );
1565
unlink $self->{'ldif_fname'};
1566
$self->{'edit_again'} = undef;
1571
# load changes into a new array for comparison
1573
open LDIF, "$self->{'ldif_fname'}" or return;
1574
my @new_ldif = <LDIF>;
1580
my $line = shift || $_;
1581
return if $line =~ /^\#/; # ignore comments
1582
my ( $attr, $val ) = ( $1, $2 ) if $line =~ /^(.+?): (.*)$/;
1583
return unless $attr;
1584
return if index($attr, ':') != -1; # ignore base64
1585
return ( $attr, $val );
1588
$self->unwrap_line( \@orig_ldif );
1589
$self->unwrap_line( \@new_ldif );
1591
my $diff = Algorithm::Diff->new( \@orig_ldif, \@new_ldif );
1593
while ( $diff->Next() ) {
1594
next if $diff->Same();
1595
my $diff_bit = $diff->Diff();
1600
if ( $diff_bit == 1 ) {
1601
foreach ( $diff->Items(1) ) {
1602
my ( $attr, $val ) = $parse->( $_ ) or next;
1603
$self->debug("DELETE: $_");
1604
$e->delete( $attr => [ $val ] );
1608
# attr insertion hunk
1610
if ( $diff_bit == 2 ) {
1611
foreach ( $diff->Items(2) ) {
1612
my ( $attr, $val ) = $parse->( $_ ) or next;
1613
$self->debug("INSERT: $_");
1614
$e->add( $attr => $val );
1620
if ( $diff_bit == 3 ) {
1622
# modification to existing line
1624
foreach ( $diff->Items(2) ) {
1625
my ( $attr, $val ) = $parse->( $_ ) or next;
1626
$self->debug("MODIFY: $_");
1628
my $cur_vals = $e->get_value( $attr, asref => 1 ) || [];
1629
my $cur_valcount = scalar @$cur_vals;
1630
next if $cur_valcount == 0; # should have been an 'add'
1632
# replace immediately
1634
if ( $cur_valcount == 1 ) {
1635
$e->replace( $attr => $val );
1639
# retain attributes that allow multiples, so updating
1640
# one attribute doesn't inadvertently remove others with
1643
next if $seen_attr{ $attr };
1645
foreach my $line ( @new_ldif ) {
1646
my ( $new_attr, $new_val ) = $parse->( $line ) or next;
1647
next unless $new_attr eq $attr;
1648
$seen_attr{ $attr }++;
1649
push @new_vals, $new_val;
1652
$e->replace( $attr => \@new_vals );
1656
# deletion within the same hunk
1658
foreach ( $diff->Items(1) ) {
1659
my ( $attr, $val ) = $parse->( $_ ) or next;
1660
my $cur_vals = $e->get_value( $attr, asref => 1 ) || [];
1661
my $cur_valcount = scalar @$cur_vals;
1663
next if $cur_valcount == 1;
1664
next if $seen_attr{ $attr };
1665
$self->debug("DELETE: $_");
1666
$e->delete( $attr => [ $val ] );
1671
my $update = sub { return $e->update( $self->ldap ); };
1672
my $rv = $self->with_retry( $update );
1673
print $rv->error(), "\n";
1675
if ( $rv->code() != LDAP_SUCCESS && $self->prompt_edit_again() ) {
1676
$self->{'edit_again'} = 1;
1677
return $self->run_edit( $dn );
1680
unlink $self->{'ldif_fname'};
1681
$self->{'edit_again'} = undef;
1686
### Display current tunable runtime settings.
1692
foreach ( sort @{ $self->{'env'} } ) {
1694
print $conf->{$_} ? $conf->{$_} : 0;
1705
my ( $key, $val ) = @_;
1707
( $key, $val ) = split /=/, $key if $key && ! defined $val;
1708
return unless $key && defined $val;
1711
$conf->{$key} = $val;
1716
### Search across the directory and display matching entries.
1721
my ( $recurse, $filter, $base ) = @_;
1724
unless ( $recurse && $recurse =~ /\-r|recurse/ ) {
1725
# shift args to the left
1726
( $recurse, $filter, $base ) = ( undef, $recurse, $filter );
1729
$filter = Net::LDAP::Filter->new( $filter );
1730
unless ( $filter ) {
1731
print "Invalid search filter.\n";
1736
$base = $self->base() if ! $base or $base eq '*';
1739
print "No search base specified.\n";
1743
# convert base path to DN
1744
$base = $self->path_to_dn( $base );
1746
$self->debug("Filter parsed as: " . $filter->as_string() . "\n");
1748
my $s = $self->search({
1749
scope => $recurse ? 'sub' : 'one',
1754
foreach my $e ( @{ $s->{'entries'} } ) {
1763
### Override internal help function with pod2usage output.
1767
return Pod::Usage::pod2usage(
1768
-exitval => 'NOEXIT',
1770
-sections => 'SHELL COMMANDS'
1775
### Generate and display a list of LDAP entries, relative to the current
1776
### location the command was run from.
1786
my ( $recurse, $long );
1788
# parse arguments: [ <option> ...] [<filter> ...] [<attribute> ...]
1790
if ( scalar @args ) {
1791
# options: support '-l' or '-R' listings
1792
if ( $args[0] =~ /^\-(\w+)/o ) {
1794
$recurse = $flags =~ /R/;
1795
$long = $flags =~ /l/;
1801
# get filter elements from argument list
1803
while ( @args && $self->is_valid_filter($args[0]) ) {
1804
push( @filters, shift(@args) );
1807
# No filter for display? Default to all entries.
1808
push( @filters, '(objectClass=*)' ) unless scalar @filters;
1810
# construct OR'ed filter from filter elements
1811
$filter = $self->make_filter( \@filters );
1813
# remaining arguments must be attributes
1814
push( @attrs, @args );
1817
# Get all attributes if none are specified, and we're in long-list mode.
1818
push( @attrs, '*' ) if $long && ! scalar @attrs;
1820
my $s = $self->search({
1821
scope => $recurse ? 'sub' : 'one',
1824
attrs => [ @attrs, 'hasSubordinates' ]
1826
if ( $s->{'code'} ) {
1827
print "$s->{'message'}\n";
1831
# if an entry doesn't have a description field,
1832
# try and show some nice defaults for ls -l !
1834
# objectClass -> Attribute to show
1839
posixAccount => 'gecos',
1840
posixGroup => 'gidNumber',
1841
ipHost => 'ipHostNumber',
1848
my $base = $self->base();
1849
foreach my $e ( sort { $a->dn() cmp $b->dn() } @{ $s->{'entries'} } ) {
1851
next if lc( $dn ) eq lc( $base );
1854
# strip the current base from the dn, if we're recursing and not in long mode
1856
$dn =~ s/,$base//oi;
1859
# only show RDN unless -l was given
1861
$dn = canonical_dn( [shift(@{ldap_explode_dn($dn, casefold => 'none')})], casefold => 'none' )
1865
# if this entry is a container for other entries, append a
1867
$dn .= '/' if $e->get_value('hasSubordinates') &&
1868
$e->get_value('hasSubordinates') eq 'TRUE';
1870
# additional arguments/attributes were given; show their values
1872
if ( scalar @args ) {
1873
my @elements = ( $dn );
1875
foreach my $attr ( @args ) {
1876
my @vals = $e->get_value( $attr );
1877
push( @elements, join(',', @vals) );
1880
print join( "\t", @elements )."\n";
1886
my $desc = $e->get_value( 'description' );
1888
$desc =~ s/\n.*//s; # 1st line only
1892
# no desc? Try and infer something useful
1897
# pull objectClasses, hash for lookup speed
1898
my @oc = $e->get_value( 'objectClass' );
1900
map { $ochash{$_} = 1 } @oc;
1902
foreach my $d_listing ( sort keys %descs ) {
1903
if ( exists $ochash{ $d_listing } ) {
1904
my $str = $e->get_value( $descs{ $d_listing }, asref => 1 );
1905
$dn .= ' (' . (join ', ', @$str) . ')' if $str && scalar @$str;
1915
print "\n$dn_count " .
1916
( $dn_count == 1 ? 'object.' : 'objects.') .
1922
### Create a new organizationalUnit entry.
1930
print "No 'directory' provided.\n";
1934
# normalize name, if it is not yet a legal DN
1935
$dir = 'ou=' . $dir unless canonical_dn( $dir );
1937
# convert given path to full DN
1938
$dir = $self->path_to_dn( $dir );
1940
# get RDN: naming attributes (lower-case) and their values
1941
my %rdn = %{ shift(@{ ldap_explode_dn($dir, casefold => 'lower') }) };
1945
return $self->ldap()->add( $dir, attr => [
1946
objectClass => [ 'top', 'organizationalUnit' ], %rdn
1950
my $rv = $self->with_retry( $mkdir );
1952
print $rv->error(), "\n";
1953
$self->update_entries( clearcache => 1 );
1958
### Alter an entry's DN.
1963
my ( $s_dn, $d_dn ) = @_;
1966
print "No source dn provided.\n";
1970
print "No destination dn provided.\n";
1974
# convert given source path to DN
1975
$s_dn = $self->path_to_dn( $s_dn );
1977
unless ( $self->is_valid_dn( $s_dn ) ) {
1978
print "No such object\n";
1982
# see if we're moving the entry to a totally new path
1983
my ( $new_dn, $old_dn );
1984
( $d_dn, $new_dn ) = ( $1, $2 ) if $d_dn =~ /^([\-\w=]+),(.*)$/;
1985
$old_dn = $1 if $s_dn =~ /^[\-\w=]+,(.*)$/;
1988
return $self->ldap()->moddn(
1992
newsuperior => $new_dn
1995
my $rv = $self->with_retry( $moddn );
1996
print $rv->error(), "\n";
1999
$self->{'cache'}->{ $new_dn } = {} if $new_dn;
2000
$self->{'cache'}->{ $old_dn } = {} if $old_dn;
2001
$self->update_entries( clearcache => 1 );
2006
### Change the 'userPassword' attribute of an entry, if
2007
### supported by the LDAP server.
2012
my $dn = shift || $self->base();
2014
$self->{'root_dse'} ||= $self->ldap->root_dse();
2016
my $pw_extension = '1.3.6.1.4.1.4203.1.11.1';
2017
unless ( $self->{'root_dse'}->supported_extension( $pw_extension ) ) {
2018
print "Sorry, password changes not supported by LDAP server.\n";
2022
# convert given path to DN
2023
$dn = $self->path_to_dn( $dn );
2025
my $s = $self->search( { base => $dn, scope => 'base' } );
2026
if ( $s->{'code'} ) {
2027
print $s->{'message'}, "\n";
2030
my $e = ${ $s->{'entries'} }[0];
2032
unless ( $e->exists('userPassword') ) {
2033
print "No userPassword attribute for $dn\n";
2037
print "Changing password for $dn\n";
2038
Term::ReadKey::ReadMode 2;
2039
print "Enter new password: ";
2040
chomp( my $pw = <STDIN> );
2041
print "\nRetype new password: ";
2042
chomp( my $pw2 = <STDIN> );
2044
Term::ReadKey::ReadMode 0;
2046
if ( $pw ne $pw2 ) {
2047
print "Sorry, passwords do not match.\n";
2051
my $setpw = sub { return $self->ldap->set_password( user => $dn, newpasswd => $pw ); };
2052
my $rv = $self->with_retry( $setpw );
2054
if ( $rv->code() == LDAP_SUCCESS ) {
2055
print "Password updated successfully.\n";
2058
print "Password error: " . $rv->error() . "\n";
2065
### Display the current working "directory".
2070
print $self->base() . "\n";
2075
### Display the currently bound user.
2080
my $msg = ( $conf->{'binddn'} || 'anonymous bind' ) . ' (' . $conf->{'server'} . ')';
2086
### Show basic information for an entry (DN) or list of objectClasses.
2088
### structural/auxillary classes
2089
### required attributes
2090
### optional attributes
2097
my ( $must_attr, $may_attr );
2100
print "No DN or objectClass(es) provided.\n";
2104
# "Magic" argument that dumps all raw schema information.
2106
if ( $dn eq '_schema' ) {
2107
$self->{'schema'}->dump();
2111
# one argument -- if it successfully resolves to a valid DN, fetch
2112
# the objectClass list from it.
2114
if ( scalar @ocs == 1 ) {
2115
$dn = $self->base() if $dn eq '.';
2116
$dn = $self->path_to_dn( $dn );
2118
my $s = $self->search({ base => $dn, vals => 1, attrs => ['objectClass'] });
2119
if ( $s->{'code'} == LDAP_SUCCESS ) {
2120
my $e = ${ $s->{'entries'} }[0];
2121
@ocs = $e->get_value('objectClass');
2125
# get the complete attributes list.
2127
( $must_attr, $may_attr ) = $self->fetch_attributes( \@ocs );
2128
my %must = map { $_ => 1 } @{$must_attr};
2130
# Output objectClass chains and flags.
2132
print "ObjectClasses:\n";
2133
foreach my $oc ( sort @ocs ) {
2134
my @sups = $self->findall_supers( $oc );
2136
my @oc_chain = ( $oc, @sups );
2139
foreach my $oc ( @oc_chain ) {
2140
my $oc_obj = $self->{'schema'}->objectclass( $oc );
2141
next unless $oc_obj;
2143
$oc = $oc . ' (' . 'structural' . ')' if $oc_obj->{'structural'};
2144
push( @oc_out, $oc );
2147
print " " . join( ' --> ', @oc_out ) . "\n" if scalar @oc_out;
2150
# Output attributes and flags.
2152
print "\nAttributes:\n";
2153
foreach my $attr ( sort (@{$must_attr}, @{$may_attr}) ) {
2155
if ( $self->{'schema'}->attribute( $attr )->{'single-value'} ) {
2156
push ( @flaglist, 'single-value' );
2159
push ( @flaglist, 'multivalue' );
2162
push ( @flaglist, $must{$attr} ? 'required' : 'optional' );
2165
$flags = (' (' . join( ', ', sort @flaglist ) . ')') if scalar @flaglist > 0;
2167
printf( " %s%s\n", $attr, $flags );
2175
### Recursively walk an objectClass heirarchy, returning an array
2181
my $oc = shift or return;
2184
foreach my $sup ( $self->{'schema'}->superclass($oc) ) {
2185
push( @found, $sup );
2186
push( @found, $self->findall_supers( $sup ) );
2194
########################################################################
2196
########################################################################
2203
my $VERSION = '1.1.0';
2208
eval 'use Term::ReadLine::Gnu';
2209
warn qq{Term::ReadLine::Gnu not installed.
2210
Continuing, but shelldap is of limited usefulness without it.\n\n} if $@;
2212
# get config - rc file first, command line overrides
2214
$conf = load_config() || {};
2215
Getopt::Long::GetOptions(
2228
'tls', 'debug', 'version',
2230
Pod::Usage::pod2usage(
2232
-message => "\n$0 command line flags\n" . '-' x 65
2238
if ( $conf->{'version'} ) {
2239
print "$0 $VERSION\n";
2243
# additional/different config file?
2245
if ( $conf->{'configfile'} ) {
2246
my $more_conf = load_config( $conf->{'configfile'} );
2247
while ( my ($k, $v) = each %{$conf} ) { $conf->{ $k } = $v }
2252
$conf->{'configfile'} ||= "$ENV{'HOME'}/.shelldap.rc";
2253
$conf->{'cacheage'} ||= 300;
2254
$conf->{'timeout'} ||= 10;
2256
# create and enter shell loop
2257
my $shell = LDAP::Shell->new();
2260
### load YAML config into global conf.
2264
my $confpath = shift;
2267
unless ( $confpath ) {
2269
"$ENV{'HOME'}/.shelldap.rc",
2270
'/usr/local/etc/shelldap.conf',
2271
'/etc/shelldap.conf',
2273
foreach ( @confs ) {
2280
$confpath or return undef;
2282
open YAML, $confpath or return undef;
2285
$data = <YAML>; # slurp!
2289
eval { $conf = YAML::Syck::Load( $data ) };
2290
die "Invalid YAML in $confpath\n" if $@;