There's a topic that I've seen arise on the chatterbox a number of
times, and that's the issue of freelance consulting. How to do it,
what rates to charge, and what gotchas there exist in the works.
During one such chatterbox conversation, ybiC asked if I could
write a few words on the topic, as he and others might find them
useful.
This is a (*ahem*) short discussion about consulting, particularly my
experiences working as a consultant. Some of what's said in
here might be useful to you, and some might be entirely the
wrong advice for the situation at hand. Like any advice, you
should take it with a sprinkle of salt.
Before you start.
Before you start consulting, be aware that it's not the
high-income, low-work, relaxing lifestyle that often it's thought
to be. It can be all those things if you're lucky or
play your cards right, but it can also be none of them. Consulting
involves accounting, project management, client liason, tax
matters, invoicing, and luck. Most jobs you try for simply won't get off
the ground, or will take forever to start. When I first started
writing this I was travelling out to Ballarat to work on a job
which was first discussed over eight months ago, and stayed up
last night working on contracts for a project that's been brewing
for over two years.
Having said that, most people end up in consulting by chance,
rather than choice. Let's look at the most common way this starts.
XYZ has asked me to do a project. What do I do now?
This is where most people start with consulting, it's certainly
where I started very many years ago. It's also the question
I most commonly see on the chatterbox.
One day, someone comes to you and asks if you're available for
a particular project, you express your interest and
discuss the matter with them. It's something that you're capable
and well-suited to doing, but you've never done consulting like this
before. After a while, the question comes up about money. How
much will this cost? How soon can it be ready?
The matter of cost is a difficult one to answer. As a general rule,
you want to figure out how much you'd like to be paid on a per-hourly
basis. If you don't know what that should be, then take what you
get paid for your "regular job", and double it. This may seem like
a lot, but unless your workplace is run by facists, you normally get
paid to do all sorts of things besides your main line of work, such as
sleeping in, or attending critical but useless meetings.
When people hire you as a consultant, they don't expect to pay for you
to sleep in, or stare sleepily into that monitor after that all-night
Quake game. Consultants are also a convenience, they are hired
for a job, and disspaear once it's gone. As such, your customers
should be prepared to pay you a little extra for your time.
Can you give me a quote?
Most clients don't care about your hourly rate,
they care about your total cost. If you can get a client
to pay you by the hour, then you're laughing, as it's very hard to
go wrong with such an agreement. Sometimes clients can be scared
by your hourly rate, and judging whether or not this will be
the case is difficult to tell.
Unless you're very fortunate, your client will want a
quote (either written or verbal) of some kind for the total
project cost.
This is where everything gets hairy. As soon as you give a quote,
most clients will be extremely reluctant to ever pay above that.
That wouldn't usually be a problem if your client has told you
everything you need to know, but the chances of that occuring
are in the same league as being struck by lightning. It does happen,
but it's a bit of a shock when it does.
So, you've given your quote, the client is happy, work commences,
and half way through the project the client asks when the
flash animation and musical score for their website is going to
be finished, since they're doing their product launch on Tuesday.
You rightly claim that you know nothing of this, the client
insists that musical scores are an obvious requirement
for a conference registration form, and the relationship goes downhill
from there.
The key is to make sure that your quote is in writing, covers what
you will (and possibly will not) do, and a list of acceptance
critera, as well as the price. You also would like your client's
signature on there somewhere. A well-written quote is your
best defense against mis-understandings, and should be written
in conjunction with your client. Find out as much as you can
about what they want, ask them lots of questions, and suggest
alternatives. You want to be quoting on what the client really
wants. Some clients won't really know what they want, so you'll have
to provide a little guidance. Some clients are incredibly sure. Some
of you with software engineering backgrounds should already have
experience here. Some clients won't really know what they want, so
you'll have to provide a little guidance. Some clients are incredibly
sure, although they might change their mind later.
If you think that writing quotes is a lot of work, you're right.
You can try avoiding it, but unless you know your client extremely
well, and have worked with them in the past, expect for at least
one of you to be dissapointed by the end of it. Dissapointed
clients never come back, and repeat customers form a significant
revenue stream for consultancies. Obviously the larger the
project, the more essential it is
Unfortunately, it's at the quoting stage of the process that most of your
clients (or you) will lose interest. Writing and reviewing quotes
is dull and boring stuff, and nobody likes to do it. Your client
may realise there are many more things they wish to think about,
or in the light of the quoting process the project is much more
expensive or difficult than they previously thought.
One way of getting around the problem of failing client interest is
to break the project up into stages, with the end of the first
stage being a usable but humble product. The client may be alarmed
at the full cost of the project, whereas they could be quite
comfortable with the costs and benefits of the stripped down
"stage 1".
Another thing worth mentioning about quoting or any other kind of
client management, is that it's worth doing everything promptly.
A project where the ball is always moving and the client always
has a quick response is much more likely to be successful. If
you're busy then a short note to say that you've received the
query and an indication of when you'll been attending it is often
appreciated.
Quoting styles
"Never ever ever do fixed price work."
--- Kirrily "Skud" Robert.
As a rule of thumb, that's good advice. Most starting
consultants have a tendancy to quote low, and it's only afterwards
that they discover that things are more difficult, or the goal-posts
have moved. A fixed price quote makes it difficult to do a good
job, you have a strong incentive to refuse extra features or do
more work than necessary, in order to avoid time blow-outs which
you won't be compensated for. This is bad for you and bad for your
client.
The preferred quoting style I use when it's impossible or undesirable
to negotiate an hourly rate is what's called a "ranged quote". A
ranged quote breaks a project up into sections and gives a price range
on each section. You can guarantee that the cost of each section will
fall within this range. Because the total cost is now flexible, you
now have much greater freedom to accept new features that appear
part-way through, and you're likely to be compensated for any extra
work that needs doing. If everything goes smoothly, the client gets
the project at a lower cost, which will make them very happy.
You can still blow-out on time with a ranged quote, so be careful.
The prototype
One of the greatest powers at your disposal is the prototype, a
mock-up of the final system with some superficial whistles and
bells to show how it works. Clients go nuts over these things,
a prototype can really impress and bolster a lot of confidence.
The only catch with prototypes is that clients invariably feel that
once they've seen a prototype, the project is almost completed.
Prototypes are like cars with no electronics and nothing under
the hood, except that with most projects the client isn't able
to "look under the hood" and see how much work is left to be done.
Some words on clients...
There are many different types of clients in the world, and they all
have different expectations. Ma and Pa's fish'n'chip shop will
behave very differently to a national telco, and treating one like
the other will get you nowhere. Make sure that you treat your
client appropriately, as many simply won't take you seriously
otherwise.
Also, be prepared for clients who are just plain bad. Some people
may meet with you to get your recommendations, but then do the
work in-house based upon what you've said. Some clients will
dissapear when it comes to paying money. Some clients mean well,
but will move project requirements in the same way that a taxi
moves around town.
If you've never worked with a client before, you might want to ask
for a deposit (anything from 10-50%) up-front, before the project
begins. For a large project, setting milestones and agreeing upon
payments for each of them is a very wise idea. Most clients
who do a runner are far away, and only run once the project has
been delivered. Consider demonstrating the product to the
client without delivering it to them until final payment is made.
This "look but don't touch" approach is often a sure-fire way
of getting money from lazy payers.
##########################################################################
(copied from fsn's scratchpad)
Regarding Exchange and SMTP, ybiC, I might have given you false hopes here, but I'll try my best.
My setup was different from yours, as I didn't actually run the Exchange system, I ran the Sendmail gateway/virusscanners that were in front of it, connecting to the Internet. This was for a large, international corporation, and we fronted a lot of different internal mail systems in addition to the Exchange systems, ie. Lotus Notes, Sendmail, OpenMail etc. A mess.
Now to your problem. I think you are either having a protocol problem or a server selection problem. Some questions:
- Do you get any error messages when you run the script?
- Does it fail immediately or do you get timeouts?
- Can you turn on debugging of a Net::SMTP session to see what's really happening? How far in the SMTP session are you coming?
- In a worst case scenario, send me a packet trace of the session, for example with ethereal, and I can probably tell you what's wrong (msg me for my email address).
- Are you sure you are connecting to the right server? Try setting $target, as in $smtp = new Net::SMTP($target) in your script, to your Exchange server (Read in my rant at the bottom why this is always a good idea).
- Are you getting any messages in the Exchange system logfiles?
- What happens if you try and send an email "manually" using telnet? It should be something like :
> telnet mail.myfineserver.com 25
Escape character is '^]'.
220 mail.myfineserver.com ESMTP Welcome to my fine server
helo client.myfineserver.com
250 mail.myfineserver.com Hello client.myfineserver.com 1.2.3.257, pleased to meet you
mail from: me@myfineserver.com
250 2.1.0 me@myfineserver.com... Sender ok
rcpt to: you@myfineserver.com
250 2.1.5 you@myfineserver.com... Recipient ok
data
345 Enter mail, end with "." on a line by itself
This is just a test, please ignore.
.
250 2.0.0 g6IAv6e18633 Message accepted for delivery
quit
221 2.0.0 mail.myfineserver.com is closing connection.
Hope this can help you, and if you need any more help, you are more than welcome to msg me again.
Even though I'm close to begin a rant here, I would like to comment on the script itself. In my opinion, a MUA like this script should always deliver to a server on the local machine. In other words, the $smtp = new Net::SMTP($target) should always use localhost as $target. The reason is that if you go directly to a remote server from a non-persistent script like this, you loose all the fine redundancy built into SMTP and DNS. Deliver all mails to a local SMTP server, and let it handle things like resending to next server in MX list, requeueing etc. etc. And this also is true if your local site has more than one SMTP server for redundancy. At the very least, I think $target should be fixed to a local server the you or your organization has control over.
##########################################################################
ybiC: Any comments on this article subrefs and dispatch tables?
wog: &$sref; is not the same as &$sref() in a big way. Also that one can use {}s like &{$sref} should be mentioned.
wog notes that &$sref() is the one equivilent to $sref->() of course.
ybiC: Would you say the example of dispatch table in that article is clean perl? If so, I can think of a script or two of mine that might could stand a few fewer if/else's.
Zaxo: looks ok to me, haven't checked details
Petruchio: Well, I prefer lexically scoped subroutines to lexically scoped references to package subroutines, where appropriate...
Petruchio: So right off, I would be more inclined to say my $foo = sub { print "Foo!\n" }; where I didn't have to call the sub from another package.
Petruchio: Particularly if you're going to be calling such things in lexicals anyway.
ChemBoy: $main::{"lexically scoped subs"}++
Petruchio: Dispatch tables are fine things, and can indeed simplify your code. The way they're doing it in that article may be the most appropriate way, depending on the situation.
tilly: If you always use lexically scoped subs, then strict.pm catches typos in subroutine names!
Petruchio: Well, let's say *an* appropriate way. The 'most' is always a bold statement. :-)
tilly: Oh, and you get nested subroutines! (No more naming fights between lexically scoped and global names.)
ybiC: So could one use a dispatch table with $foo instead of \&foo then? Or is that statement utter drivel?
Petruchio: Well, yes... except that my next suggestion was going to be to put them directly into a hash. :-) Strict won't, of course, help you there.
Zaxo: ybiC it's true
tilly: One could use a dispatch table with $foo. In fact one can even build up the dispatch table with functions. I have written quite a few functions that return hashes of key/handler, key/handler.
Petruchio: Yes, ybiC... by saying my $x = \&foo; you make a lexical $x hold a reference to a package subroutine called &foo. If you never need to use the subroutine as a package subroutine, you'd might as well not create it that way in the first place
Zaxo: also, you can define anon subs directly in the hash: %dt=(foo=>sub{join"bar",@_})
Petruchio: But then, I'm a bit unusual for preferring, by default, an anonymous sub in a lexical variable to a package variable. You won't find most people doing that. I think that should be made clear before I recommend my preference.
Zaxo suddenly think Petruchio already said that
Petruchio: Zaxo is correct. But as someone just mentioned, long subroutines will create formatting problems if you try to put them all into the hash declaration. This is why I often first declare my %sub; and then on later lines declare the subs.
Petruchio: I've put an example of what this looks like on my scratchpad
#!/usr/bin/perl -w
use strict;
{
my %sub;
$sub{foo} = sub{
whatever;
};
$sub{bar} = sub{
whatever;
whatever;
};
$sub{bat} = sub{
whatever;
};
$sub{moo} = sub{
whatever;
whatever;
whatever;
};
}
##########################################################################
- known MAC addresses .1.3.6.1.2.1.17.4.3.1.1.0
- bridge ports .1.3.6.1.2.1.17.4.3.1.2.0
- bridgeports to ifIndex .1.3.6.1.2.1.17.1.4.1.2
- ifIndex to ifDescr .1.3.6.1.2.1.2.2.1.2
- ifIndex to ifName .1.3.6.1.2.1.31.1.1.1.1
#! /usr/bin/perl -w
# netsnmpt.pl
use strict;
use Net::SNMP(qw(snmp_event_loop oid_lex_sort));
my @targets = @ARGV;
my @oid = (
'.1.3.6.1.2.1.2.2.1.1',
'.1.3.6.1.2.1.31.1.1.1.1',
'.1.3.6.1.2.1.2.2.1.7',
'.1.3.6.1.2.1.2.2.1.8',
);
# ifIndex
# ifName
# ifAdminStatus
# ifOperStatus
my ($session, $error, $response);
for my $target(@targets) {
($session, $error) = Net::SNMP -> session(
-hostname => $target,
-community => 'public',
);
print($session -> hostname(), "\n");
for my $oid(@oid) {
if (defined($response = $session -> get_table($oid))) {
for my $value(oid_lex_sort(keys(%{$response}))) {
print($response -> {$value}, "\n");
}
} else {
print($session -> error(), "\n");
}
}
}
##########################################################################
Oversimplified networky terms and descriptions:
JACK is a layer 1 (physical) network receptacle. ie; cubicle jack
PORT is a layer 2 (datalink) network receptacle. ie; switch or hub port
INTERFACE is a layer 3 (network) network receptacle. ie; router interface
LINK is the wired/fibered connection between two electronic network devices.
FRAME is the layer 2 unit of network data. ie; switches pass frames.
PACKET is the layer 3 unit of network data. ie; routers forward packets.
BANDWIDTH is the maximum number of bits that can traverse a given link in one second: Gbps, Mbps, Kbps, bps. <update> Bandwidth limitations are usually a problem for low-speed WAN links and for apps like SCP and FTP where many large packets are crossing the wire. Bandwidth is not to be confused with Bus Speed which is measured in Bytes per second; GBps, MBps, KBps, Bps. Network links can be thought of as *serial* connections, as opposed to computer internal busses which are 8 or more bits wide.</update>
LATENCY is the delay added for packet to traverse a link, or by a router interface in transmitting a packet onto a link, or receiving a packet from a link. Is greatest problem when distance between devices is large (speed of light), and for low speed links (insertion delay), and for applications like SSH and telnet where a mess o' small packets are flying about.
HUBs repeat all received frames to all other ports. So they share internal bandwidth amongst all connected devices, but add no latency to passing frames. Unless of course bandwidth utilization is saturated and collisions and/or drops occur. Same IP subnet across all hub ports. Generally appropriate for segment of >= 50 PCs with fairly light traffic levels.
SWITCHes dedicate full wirespeed bandwidth to each port, and have backplane bandwidth far exceeding any one port. But not necessarily meeting bandwidth of all ports together. No latency added to passing frames unless bandwidth utilization of the port is saturated, so that buffering and/or drops occur. Note that switchport saturation can normally only occur on *uplink* ports where multiple load from multiple devices is aggregated into one port. Same IP subnet across all switch ports. Generally appropriate for up to around 200 PCs depending on broadcast traffic levels.
ROUTERs can add significant latency, as the CPU has to process every packet. Generally used in LANs to interconnect hub or switch segments/subnets. Routers limit scope of broadcast domain for switched LAN, and also to limit scope of collision domain in hubbed LAN. Different IP subnet on each router interface.
BANDWIDTH SATURATION occurs when more traffic is generated than can be passed/forwarded by the hub/switch/router.
- for hubs, saturation occurs when total traffic from all connected devices approaches around 40% of rated bandwidth, at which point collisions+retransmissions occupy most of the bandwidth.
- for switches, saturation can occur at the switchfabric if it's less than the sum of all ports, or for example at an uplink port if off-switch traffic generated by all other ports exceeds rating of the uplink port. When saturation occurs, a switch will buffer frames as much as possible, and drop frames when buffers fill. Commercial quality switches are considered "wire-speed" because they make frame-passing decisions in hardware ASICs.
- for primitive routers saturation is often a CPU performance limitation, since that's where packet forwarding decisions occur.
HALF-DUPLEX vs. FULL-DUPLEX refers to whether a port/interface can transmit and receive simultaneously. All hub ports are limited to HDX by their very design. Switch ports can be HDX or FDX, depending on quality of the switch, and the same for router interfaces. FDX provides possibility of significantly better throughput than HDX since acknowledgments don't interupt flow of data packets.
##########################################################################
Planned family of Perl modules for Cisco switch+router maintenance
Cisco
|
`-- Utils
|-- Bandwidth.pm
|-- Gui.pm
|-- SetPass.pm
|-- ShoIfStat.pm
|-- ShoErrDis.pm
|-- ShoArpT.pm
|-- ShoCamT.pm
|-- ShoCdpN.pm
|-- ShoIpRoute.pm
|-- ShoSerNum.pm
|-- ShoVer.pm
|-- TestSuite.pm
`-- Internal
|-- DateTime.pm
|-- PrintLogConsole.pm
|-- GetFromUser.pm
|-- Unique.pm
|-- WrapSnmp.pm
`-- WrapTelnet.pm
Code to extract the modules from
Existing modules+libs possibly needed
Questions about modules
- Is it bad form to have lexical vars of same name/purpose in multiple functions of one module?
- Pros/cons of several single-purpose modules vs fewer number of somewhat consolidate modules?
- Somewhere I got the impression that OOP is a mainstay of perl modules. The scripts I am extracting code from to make these modules are mostly procedural. Might this pose problems?
- Advice on writing a family of modules?
tye 'I would run h2xs separately for each module and steal the top-level build script from some dist that already has one (: (libwin32, for example). I always end up reading source code in lib/ExtUtils when I am making modules, tho'
crazyinsomniac
Q1: not really (unless they do reference radically different things, in which case it might get confusing. remember, self documenting code)
Q3: if you need to be carrying around something like a socket, you wanna go oo...
Q4: don't forget to write Cisco::Util::TestSuite
toma - http://tomacorp.com/perl/xs.html
http://www.nevis.columbia.edu/~winter/howto/howto_create_e910_perlmod.html
Info (mostly PM nodes) on writing modules
Existing modules+libs possibly needed for GUI dev
Info on building GUI for Perl code
DateTime.pm
package Cisco::Utils::Internal::DateTime;
$VERSION = 0.02.00;
@EXPORT_OK = qw(DateTime);
use strict;
use base 'Exporter';
sub DateTime {
my($sec,$min,$hour,$mday,$mon,$year) = localtime(time);
my $datetime = sprintf(
"%04d-%02d-%02d %02d:%02d:%02d",
$year+1900,$mon+1,$mday,$hour,$min,$sec
);
}
1;
=head1 NAME
DateTime
=head1 SYNOPSIS
DateTime is intended for use by Cisco::Utils modules. It's not
meant for use directly from Perl scripts, and it's interface may chang
+e
at any time without notice.
DateTime accepts no arguments, and returns a scalar containing
year, month, mday, hour, minute, second in human-readable format.
YYYY-MM-DD hh:mm:ss
#!/usr/bin/perl -w
use strict;
use Cisco::Utils::Internal::DateTime qw(DateTime);
my $datetime = DateTime() or die "Error: $!";
print "$datetime\n";
=head1 UPDATE
2001-11-18 07:40 CDT
Original working code.
=head1 TODO
Figure out enough h2xs to package this module for distribution.
=head1 BUGS
None that I know of.
=head1 REQUIRES
Nothing aside from Perl itself.
=head1 AUTHOR
ybiC
=head1 CREDITS
Thanks to Petruchio for kickstarting me into modulitazing my Ciscoey
scripts. And to vroom for PerlMonks.
=cut
GetFromUser.pm
package Cisco::Utils::Internal::GetFromUser;
$VERSION = 0.05.20;
@EXPORT = qw(GetFromUser);
use strict;
use base 'Exporter';
use Term::ReadKey;
use Term::ReadLine;
sub GetFromUser {
my $echo = shift(@_);
my $timeout = shift(@_);
my @prompt = @_;
my $pass;
$echo = 0 unless $echo == 1;
$timeout = 60 unless(($timeout =~ /\d+/) and ($timeout >= 1));
@prompt = '' unless defined @prompt;
ReadMode('noecho') if $echo == 0;
print $_ for @prompt;
if ($^O eq 'MSWin32') { chomp ($pass = <STDIN>); }
else { $pass = ReadLine($timeout); }
ReadMode('restore') if $echo == 0;
unless(defined($pass)) {
return 'Sorry, you waited too long to enter something.';
}
chomp $pass;
return $pass;
}
1;
=head1 NAME
GetFromUser
=head1 SYNOPSIS
GetFromUser is intended for use by Cisco::Utils modules. It's not
meant for use directly from Perl scripts, and it's interface may chang
+e
at any time without notice.
GetFromUser accepts 2 scalars and 1 array for input values:
1 - echo-to-screen: 1 for yes, 0 for no (default of no)
2 - timeout seconds: integer greater than zero (default of 60)
3 - prompt: text to prompt user to provide input (default of
+ '')
User prompt is here so echo disabled *before* user is prompted for inp
+ut.
Term::ReadKey provides no-echo while user typing password.
Term::ReadLine provides timeout if wait too long.
Win32 doesn't support timeout, 2nd arg still required before prompt te
+xt.
#!/usr/bin/perl -w
use strict;
use Cisco::Utils::Internal::GetFromUser;
my $echo = 0;
my $timeout = 120;
my $favColor = GetFromUser(
$echo,
$timeout,
"\nThe old man at the ropebridge asks\n",
'Wots yer favorite color? ',
);
if $favColor eq 'Sorry, you waited too long to enter something.' {
print "\nWooooaaahh!!\n\n"
} else {
print "\n$favColor, eh? You may pass.\n\n";
}
=head1 UPDATE
2001-11-18 22:20 CDT
Original working code.
=head1 TODO
Investigate Term::Prompt for cross-platform no-echo.
Figure out enough h2xs to package this module for distribution.
=head1 BUGS
None that I know of.
=head1 REQUIRES
Term::ReadKey
Term::ReadLine
=head1 AUTHOR
ybiC
=head1 CREDITS
Thanks to Petruchio for kickstarting me into modulitazing my Ciscoey
scripts. And to vroom for PerlMonks.
=cut
PrintLogConsole.pm
package Cisco::Utils::Internal::PrintLogConsole;
$VERSION = 0.02.09;
@EXPORT_OK = qw(PrintLogConsole);
use strict;
use base 'Exporter';
sub PrintLogConsole {
my ($file, $aRefMssgItems) = @_;
open(FH, "> $file") or return "Error opening $file: $!\n";
for(@$aRefMssgItems){
print "$_\n";
print(FH "$_\n") or return "Error printing to $file: $!\n";
}
close FH or return "Error closing $file: $!\n";
}
1;
=head1 NAME
PrintLogConsole
=head1 SYNOPSIS
PrintLogConsole is intended for use by Cisco::Utils modules. It's not
meant for use directly from Perl scripts, and it's interface may chang
+e
at any time without notice.
PrintLogConsole accepts a list of two elements. The first is a scalar
of the file to print to. The second is an array reference of the
message(s) to print.
#!/usr/bin/perl -w
use strict;
use Cisco::Utils::Internal::PrintLogConsole qw(PrintLogConsole);
my $logfile = './file.log';
my @msgs = (
"Starting program run",
"La-la-la...",
"Humina humina...",
);
my $response = PrintLogConsole($logfile, \@msgs);
unless ($response = 1) {
print $response;
exit;
}
=head1 UPDATE
2001-11-17 22:25 CDT
Original working code.
=head1 TODO
Figure out enough h2xs to package this module for distribution.
=head1 BUGS
None that I know of.
=head1 REQUIRES
Nothing aside from Perl.
=head1 AUTHOR
ybiC
=head1 CREDITS
Thanks to Petruchio for kickstarting me into modulitazing my Ciscoey
scripts. And to jcwren for scalar+array passing tips. And to vroom
for PerlMonks.
=cut
Unique.pm
package Cisco::Utils::Internal::Unique;
$VERSION = 0.15.02;
@EXPORT_OK = qw(Unique);
use strict;
use base 'Exporter';
use Tie::IxHash;
sub Unique {
my @inlist = @_;
my (@uniq, %seen);
my $have_TIxH;
BEGIN {
$have_TIxH = 0;
eval { require Tie::IxHash };
unless ($@) {
Tie::IxHash->import();
$have_TIxH = 1;
}
}
tie (%seen, "Tie::IxHash") if ($have_TIxH == 1);
++$seen{$_} for(@inlist);
@uniq = keys %seen;
}
1;
=head1 NAME
Unique
=head1 SYNOPSIS
Unique is intended for use by Cisco::Utils modules. It's not
meant for use directly from Perl scripts, and it's interface may chang
+e
at any time without notice.
Unique accepts a list, and returns a list containing only the unique
elements. If Tie::IxHash is installed, the return elements are in the
same order as the list input. If not, the order of the returned list
elements are subject to Perl's unpredictable hash element ordering.
#!/usr/bin/perl -w
use strict;
use Cisco::Utils::Internal::Unique qw(Unique);
my $file = shift or die "You forgot to provide a filename!\n";
open (FH, "< $file") or die "Error opening $file for read: $!";
my @lines = <FH>;
close FH or die "Error closing $file: $!";
my @uniques = Unique(@lines) or die "Error: $!";
print "$_" for @uniques;
=head1 UPDATE
2001-11-17 19:15CDT
=head1 TODO
Check for presence of Tie::IxHash before tieing %seen.
=head1 BUGS
None that I know of.
=head1 REQUIRES
Tie::IxHash
=head1 AUTHOR
ybiC
=head1 CREDITS
Thanks to Petruchio for kickstarting me into modulatizing my Ciscoey
scripts. And to vroom for PerlMonks.
=cut