1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
|
<!--startcut ==============================================-->
<!-- *** BEGIN HTML header *** -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML><HEAD>
<title>Saving Users From Themselves -or- Dealing with User Input in Python LG #82</title>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#0000AF"
ALINK="#FF0000">
<!-- *** END HTML header *** -->
<!-- *** BEGIN navbar *** -->
<IMG ALT="" SRC="../gx/navbar/left.jpg" WIDTH="14" HEIGHT="45" BORDER="0" ALIGN="bottom"><A HREF="lg_bytes.html"><IMG ALT="[ Prev ]" SRC="../gx/navbar/prev.jpg" WIDTH="16" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="index.html"><IMG ALT="[ Table of Contents ]" SRC="../gx/navbar/toc.jpg" WIDTH="220" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../index.html"><IMG ALT="[ Front Page ]" SRC="../gx/navbar/frontpage.jpg" WIDTH="137" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue83/evans.html"><IMG ALT="[ Talkback ]" SRC="../gx/navbar/talkback.jpg" WIDTH="121" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../lg_faq.html"><IMG ALT="[ FAQ ]" SRC="./../gx/navbar/faq.jpg"WIDTH="62" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="heriyanto.html"><IMG ALT="[ Next ]" SRC="../gx/navbar/next.jpg" WIDTH="15" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><IMG ALT="" SRC="../gx/navbar/right.jpg" WIDTH="15" HEIGHT="45" ALIGN="bottom">
<!-- *** END navbar *** -->
<!--endcut ============================================================-->
<TABLE BORDER><TR><TD WIDTH="200">
<A HREF="http://www.linuxgazette.com/">
<IMG ALT="LINUX GAZETTE" SRC="../gx/2002/lglogo_200x41.png"
WIDTH="200" HEIGHT="41" border="0"></A>
<BR CLEAR="all">
<SMALL>...<I>making Linux just a little more fun!</I></SMALL>
</TD><TD WIDTH="380">
<center>
<BIG><BIG><STRONG><FONT COLOR="maroon">Saving Users From Themselves<BR>
-or-<BR>
Dealing with User Input in Python</FONT></STRONG></BIG></BIG><BR>
<STRONG>By <A HREF="../authors/evans.html">Paul Evans</A></STRONG></BIG>
</TD></TR>
</TABLE>
<P>
<!-- END header -->
<p>You probably won't be using <a href="http://python.org/">Python</a> long
before writing a program which needs user input. As a wide-eyed, innocent new
Python programmer, you may naively expect that you can simply ask users for
input and they will just give it to you....</p>
<BLOCKQUOTE><EM>
WARNING: Showing the preceding sentence to veteran programmers may
cause them to collapse on the floor giggling helplessly.
</EM></BLOCKQUOTE>
<p>Users don't work that way.</p>
<p>For example, if you ask for a simple 'y or n' response, your user may
cheerfully type in their name - or their lunch order, or nothing at all - and
your program will break. They don't do this on purpose (well, <i>mostly</i>). It's
just that the poor dears are easily distracted, totally ignore your carefully
worded input prompts and often type complete gibberish as far as your program
is concerned. Next, oddly enough, they will blame you, the programmer. Then you will look
foolish and feel Unhappy.</p>
<p>To avoid this misery, the very first thing you need to do is make sure that
whatever comes back from the user is checked to see if it's even vaguely
close to what you expected. Python has heaps of functions to help you with
this and we'll begin by going through some of them together below.</p>
<p>Another thing you can do is use <i>validators</i> on your input widgets. The way
these work is they simply throw away any keystrokes that are not what you are
after. As an example, if you set a numeric validator on a string widget,
users can press 'ABC' etc. as much as they like and nothing will even show up
in the widget. The only keys they can press that will have any effect are 0-9
and, perhaps, a decimal or dollar symbol. We'll play with these too later on.</p>
<p>Finally, even if you are lucky enough to find yourself in possession of a
particularly well-trained and obedient user who always types what you ask,
the input is unlikely to be <i>formatted</i> exactly the way you want it. Careless
typing often produces strings like 'jOHN sMith' (caps lock) or phone numbers
resembling '604555-1212'.</p>
<p>All kidding aside, it's actually <i>your job</i> as a programmer to make it as
easy and fast as possible for the user to input data and that it be presented
and stored in a consistent format. Plus, you can get a great deal of personal
satisfaction and <i>even</i>, dare I say it? <i>gratitude</i> from users if you can save
them from the hell of properly typing something like a Canadian postal code.</p>
<h2>Acquiring Input</h2>
<p>First your program will need to acquire some user input. From the console
Python offers two methods for this 'raw_input("Prompt")' and 'input("Prompt")'.
(Don't use 'input', see below.) You can also get input from good ol' command
line arguments or environment variables.</p>
<p>Other, more graphical methods are available, without getting too carried away, such as
<a href="http://xdialog.free.fr">Xdialog</a>, Gdialog
(part of gnome-utils) or <a href="http://kaptain.sourceforge.net/">Kaptain</a>.</p>
<p>Access to full-blown GUI toolkits is available from Python using
<a href="http://www.riverbankcomputing.co.uk/pyqt/">PyQT</a>
, <a href="http://pmw.sourceforge.net">TKinter</a>,
<a href="http://wxpython.org">WxPython</a> and <a href="http://www.daa.com.au/~james/pygtk/">PyGTK</a>
among others.</p>
<p>This is probably a good time to provide a few words of caution. Most users
are contented, docile creatures who like to have their belly rubbed, but you
<i>will</i> encounter rogue types bent on destruction.</p>
<p>For this reason you must never allow user input to leak into your command space:</p>
<ul>
<li><STRONG>Use 'raw_input()' instead of 'input()'.</STRONG> 'input()' is
fed to 'eval()' before your program gets it. This automatically converts
types, which is convenient if you want an integer <EM>and</EM> the user accidentally
gives you one. But the user would have to quote strings. Worse, a rogue user
might type 'os.system("rm -r *")', which would give you a bad day.
'raw_input()' returns whatever the user entered as a string. This makes
validation easier, because you know what type it will be and thus which
operations you can apply to it.<p></li>
<li><STRONG>Always check input to os.system(), os.popen() or os.exec*() calls.</STRONG>
<p></li>
<li><STRONG>Always escape user input before printing it to a web page or using it in a
SQL query.</STRONG> This is in addition to input checking. Unescaped special
characters can cause invalid HTML, screw up your page formatting, and allow the user
to exploit Javascript "features" against another user. Unescaped special characters in
a SQL query may cause a SQL syntax error or cause the query to do more
than intended. Use 'cgi.escape()' to escape HTML. See your database documentation
to escape SQL queries.
<p></li>
</ul>
<p>O.K. Relax. The spooky part is over.</p>
<p>Open an xterm and type 'python' to enter the interpreter. Note: Many of these
examples <i>require</i> that you be using a version of Python greater than or
equal to version '2'. Redhat still ships with version 1.5x as
default, so if you are a Redhat user you will need to type 'python2' instead
(and possibly install the rpm first from 'add-ons'). For the record, version
'1.5' was released in a year which began with the digits '1' and '9'.</p>
<h2>Checking the Content of String Objects</h2>
<p>Programming languages usually include methods for checking of this kind and
Python is no exception. Consider one of our first challenges as stated above:
making sure the user gives us a valid number when we ask for one.</p>
<p>It happens that all string objects in Python have built-in <i>methods</i> which
make this quite painless. Type these lines in at the '>>>' prompt:</p>
<pre>
>>>input = '9'
>>>input.isdigit()
1
</pre>
<p>This will return a '1' (true), so you can easily use it in an 'if' statement
as a condition. Some other handy attributes of this kind are:</p>
<pre>
s.isalnum() returns true if all characters in s are alphanumeric, false otherwise.
s.isalpha() returns true if all characters in s are alphabetic, false otherwise.
</pre>
<p>For a complete list of these and much more, I highly recommend the
<a href="http://www.brunningonline.net/simon/python/PQR2.1.html">Python 2.1 Quick Reference</a>. I use this all the time and even have an
older text version stuffed into <a href="http://hnb.sourceforge.net">HNB</a> for speed.</p>
<p>This will get us through simple cases like menu choices, but what if we
wanted a float or a real number?</p>
<p>Consider:</p>
<pre>
input = '9.9' or
input = '-9'
</pre>
<p>Both of these are valid numbers, but input.isdigit() will return '0' (false),
because the negative sign and the decimal point are not 'digits'. Our
poor user will be very confused when we spit back an error message if these
entries are valid.</p>
<p>So, let's <i>assume</i> that they are what we want and try to convert
them explicitly. For this we'll use the Python <i>try/except</i> construction.
Python raises <i>exceptions</i> of different kinds on errors and we can trap these
errors individually by name.</p>
<p>Say we wanted an integer like '-9', we can use the numeric operator 'int()'
to explicitly attempt the conversion for us.</p>
<pre>
try:
someVar = int(input)
print 'Is an integer'
except (TypeError, ValueError):
print 'Not an integer'
</pre>
<p>Two things to notice here. The first is that we are checking for two
different exceptions, Type and Value. This way we not only handle the user
entering a float (like '9.9'), but we also allow for the possibility that they
didn't even enter a number of any kind - perhaps they entered 'Ham on rye'.
The second thing to notice is that <i>we actually entered the kinds of
exceptions we were interested in trapping.</i> It's very easy to just type in
open ended exceptions without bothering to look up which errors you are
trapping like this:</p>
<pre>
try:
someVar = int(input)
print 'Is an integer'
except:
print 'Not an integer'
</pre>
<p>DO NOT DO THIS. Python will let you, but since you are now trapping <i>all</i>
exceptions debugging will be a nightmare for you if anything breaks. Just
trust me on this one; look up the errors you mean to trap and you'll save
time in the long run.</p>
<p>Other operators you'll find useful are long() and float(). On the flip side,
str() can convert anything to a string.</p>
<p>Don't forget to range check - it's no good congratulating yourself on
ensuring your program always gets an integer from a user if it blithely
accepts the integer '42' as a valid month day... Make sure the number falls
into the expected range using the comparison operators '>, <, >=' etc.</p>
<h2>Validating Input</h2>
<p>As we've seen, we can validate input <i>after</i> we get it, but wouldn't it be
nice if we could prevent the user from entering mistakes in the first place?</p>
<p>Enter widget validators.</p>
<p>These are things built into graphical user interface toolkits that prevent
unwanted keystrokes from even <i>appearing</i> in the string widget. Toolkits usually
come with some built-in validators for numeric, alpha, and alphanumeric etc.
and are quite easy to use. I'm currently using mostly <a href="http://www.riverbankcomputing.co.uk/pyqt/">PyQT</a>
for gui's, but <a href="http://pmw.sourceforge.net">TKinter</a>,
<a href="http://wxpython.org">WxPython</a> and even <a href="http://kaptain.sourceforge.net/">Kaptain</a>
all have validators. I could be wrong, but <a href="http://www.daa.com.au/~james/pygtk/">PyGTK</a>
seems not to have them - yet. Perhaps you could hook up a signal and roll your own if you happen to
use a toolkit that doesn't have them.</p>
<p>If the built-in validators don't suit you then PyQt, for example, allows you
to specify your own, custom validators.</p>
<p>Clearly, I can't go into detail for every toolkit out there, but here's an
example of how to attach a numeric validator to a widget in PyQT. The
widget's name is 'self.rate', we're attaching the 'QDoubleValidator' and
telling it to accept numbers between 0.0 and 999.0 up to 2 decimal places:</p>
<pre>
self.rate.setValidator(QDoubleValidator(0.0, 999.0, 2, self.rate) )
</pre>
<p>Nice eh? Notice it took care of range checking for us too!</p>
<p>Other ways to help users enter information include spinners, pick-lists and
combo-boxes, but you already knew that.</p>
<h2>Formatting Input</h2>
<p>Remember the 'jOHN sMith' example from the introduction? Here's the fix:</p>
<pre>
>>>'jOHN sMith'.title()
'John Smith'
</pre>
<p>Yes, yet another attribute of all string objects in Python is the 'title()'
attribute which will helpfully capitalize each word for you. 'capitalize()'
is similar, but only does the first character:</p>
<pre>
>>> 'jOHN sMith'.capitalize()
'John smith'
</pre>
<p>Go ahead and try 'upper()', 'lower()' and 'swapcase()' on your own if you
like. I think you can guess their behaviour.</p>
<p>But how about 'rjust(n)'? This is only one of some really handy attributes
you can use to layout reports. Watch:</p>
<pre>
>>> 'John Smith'.rjust(15)
' John Smith'
</pre>
<p>Our string has been right justified for us in a string 15 characters long.
Sweet. As you've probably guessed, there are also 'center(n)' and 'ljust(n)'.
Again, have a look at the
<a href="http://www.brunningonline.net/simon/python/PQR2.1.html">Python 2.1 Quick Reference</a>
to see them all.</p>
<p>Another, very important operator in Python is the '%' (per cent) operator.
The description of this in combination with list objects and printf-style
formatting codes could easily consume several pages, so I'm just going to gloss
over it with a few examples to pique your interest today.</p>
<p>In it's simplest form, the '%' operator lets you write, say, a proper sentence that includes variables which can change
at runtime:</p>
<pre>
>>> 'This is a %s example of its %s.' % ('good', 'use')
'This is a good example of its use.'
</pre>
<p>At least, I hope it is. This is only the beginning of its power. In addition to just string object substitution with
'%s' there is also '%r' and the printf friends from the 'C' language:
c, d, i, u, o, x, X, e, E, f, g, G.</p>
<p>Here's an example from <a href="http://www.brunningonline.net/simon/python/PQR2.1.html">Python 2.1 Quick Reference</a>:</p>
<pre>
>>> '%s has %03d quote types.' % ('Python', 2)
'Python has 002 quote types.'
</pre>
<p>The right hand side may also be a mapping, which allows you to refer to fields by name.
</p>
<p>Let's move on to something a little more challenging, but common enough.</p>
<h3>Phone Numbers</h3>
<p>Phone numbers are variable in length. Sometimes they are only 2 or 3 digits
long if you are behind a corporate PBX system. Other times they might stretch
out to 15 digits or more for international calling. They <i>might</i> even contain
'#' symbols or asterisks. Maybe even commas. Worse, the user may attempt to
impose a format on it as they enter it. Or a partial format. Or not.</p>
<p>Now, it will only frustrate your user if you don't let them at least try to
enter it properly, so your validator had better accept all of #, *, 'comma',
-, ), ( as well as the digits 0-9. Of course, you could still end up with:</p>
<pre>
'250-(555)-12-12'
</pre>
<p>instead of the string:</p>
<pre>
'(250) 555-1212'
</pre>
<p>that we actually want (for a North American phone number anyhow). Don't
worry, we'll make the solution generic enough to handle just about anything.</p>
<p>My first instinct when I need something like this is to copy someone else's
work by mining <a href="http://www.google.com">Google</a> - especially <a href="http://www.google.com">Google Groups</a>. This turns out to
be a good instinct for me to have since the code snippet I usually find will
be far better than I could do on my own. Unfortunately, this time I turned up
an email from Guido van Rossum (the inventor of Python) explaining to someone
that Python did not have such a thing and perhaps they could use something
like:</p>
<pre>
import string
def fmtstr(fmt, str):
res = [] i = 0
for c in fmt:
if c == '#':
res.append(str[i:i+1]) i = i+1
else:
res.append(c)
res.append(str[i:])
return string.join(res)
</pre>
<p>This is a darn good start of course and you can't argue with the credentials of
its author, but it doesn't handle all the cases without a lot of 'if/then'
constructs to count how many digits you were given in order to choose a format
string of the correct length. Go ahead and paste it
into your xterm and then call it like this:</p>
<pre>
>>> fmtstr('###-####', '5551212')
'5 5 5 - 1 2 1 2 '
</pre>
<p>In fact, I did copy and paste it into my editor and then constructed a long sequence of
'if/thens' for phone numbers, dates and other types of entries, but I still wasn't
handling everything. Plus, I had dozens and dozens of lines doing self-similar
things. They have since passed on to their reward.</p>
<p>O.K., here we go... First, let's filter any "extra" formatting characters we
let the user type in:</p>
<pre>
def filter(inStr, allowed):
outStr = ''
for c in inStr:
if c in allowed:
outStr += c
return outStr
</pre>
<p>We could call it like this:</p>
<pre>
>>>filter('250-(555)-12-12', string.digits)
'2505551212'
</pre>
<p>Or we could define the second argument ourselves as '0123456789#*,' to
include all the allowable characters possible.</p>
<p>Now we just take Guido's code snippet and (this is the good bit) <i>reverse</i>
both the input arguments. This way we can specify just one long format string
and it will be matched until we run out of input. Any extra input will just
get tacked on, so we will never lose any characters.</p>
<pre>
# import the regular expression module
import re
def formatStr(inStr, fmtStr, p = '^'):
inList = [x for x in inStr] #list from strings..
fmtList = [x for x in fmtStr]
# the good bit
inList.reverse(); fmtList.reverse()
outList = []
i = 0
for c in fmtList:
if c == p:
try:
outList.append(inList[i])
i += 1
# break if fmtStr longer than inStr
except IndexError:
break
else:
outList.append(c)
# handle inStr longer than fmtStr
while i < len(inList):
outList.append(inList[i])
i += 1
# put it back the way we found it
outList.reverse()
outStr = ''.join(outList)
# remove stray parens/- etc
while re.match('[)|-| ]', outStr[0]):
outStr = outStr[1:]
# close any legit parens
while outStr.count(')') > outStr.count('('):
outStr = '(' + outStr
return outStr
</pre>
<p><a href="misc/evans/fmtstr.py.txt">[Text version of this listing.]</a></p>
<p>It's basically the same as Guido's except the default placeholder character is now
a '^' (caret), because we may need to use the '#'. Alternatively, this may be
specified as an, optional, third argument if we ever need real carets in the output.</p>
<p>Here's some sample output:</p>
<pre>
>>> formatStr('51212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'5-1212'
>>> formatStr('045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'(04) 555-1212'
>>> formatStr('16045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 (604) 555-1212'
>>> formatStr('1011446045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 011 44 (604) 555-1212'
</pre>
<p>In practice, you'll probably want to simply define your phone formatting
string early on e.g.:</p>
<pre>
phone_format_str = ' ^^^ ^^ (^^^) ^^^-^^^^'
</pre>
<p>There's a space at the beginning of the string so that any additional characters won't get smooshed onto it. You'd likely call it thus:</p>
<pre>
formatStr(input, phone_format_str)
</pre>
<p>... after you clean up your 'input' with something like the 'filter()' function.</p>
<h3>Postal Codes</h3>
<p>In case you are (blessedly) unfamiliar with Canadian postal codes, they look like
this:</p>
<pre>
'V8G 4L2'
</pre>
<p>Which <i>appears</i> innocuous enough until you attempt to type it. Especially
for non-typists (like me). You can turn on the caps lock - and then forget
to turn it off - or you have to type [shift]+alpha, number, [shift]+alpha
etc. and quite often end up with: 'v*g $l@' when you get out of sequence.
Needless to say, users <i>hate</i> typing them in and they hardly ever
look right. Mostly your application won't even capture postal codes, because
users simply won't bother. Some other countries have similar post codes. Shame.</p>
<p>Now, with our new formatting function, they're a piece of cake. First, we either
validate or filter whatever they give us, then we simply use Python's
built-in string attribute 'upper()' to set the case of the alpha characters
properly, finally:</p>
<pre>
>>>formatStr('V8G4L2', ' ^^^ ^^^')
'V8G 4L2'
</pre>
<p>If accurate postal codes are critical to your application, you will need to do
more verification by way of counting the characters and verifying the pattern. For
general use though, you need to allow for the postal codes of other countries. I
think I normally format only if the number of characters == 6 after clean up.</p>
<p>How about Social Insurance Numbers? Same deal:</p>
<pre>
>>> formatStr('716555123', '^^^-^^^-^^^')
'716-555-123'
</pre>
<p>You should run a check digit routine over Social Insurance Numbers first to
ensure they are valid. Ditto for credit cards.</p>
<p>I hope these examples will save you some time in coding user interfaces. I'd
very much like to <a href="mailto:pevans@catholic.org?subject=LG article on User Input">hear back</a>
with examples or improvements of your own. Particularly ways of dealing with dates<SUP><A HREF="#foot1">1</A></SUP> with users. They're always fun.</p>
<p>By the way, it's very important that you not keep these formatting aids a secret from your users. Put it in the 'help', use
'tooltips' or 'whatis' to let them know the facility is there for them. If they find out after months of typing things the long way,
they are liable to pout and you'll end up wasting afternoon coffee scratching them behind the ears (morning coffee is a given).</p>
<p>Have fun with it!</p>
<A name="foot1">1</A> That's <i>calendar</i> dates...
<!-- *** BEGIN bio *** -->
<SPACER TYPE="vertical" SIZE="30">
<P>
<H4><IMG ALIGN=BOTTOM ALT="" SRC="../gx/note.gif">Paul Evans</H4>
<EM>Paul Evans loves everything about electronics and computers in particular. He
is old enough to remember drooling over an Altair 8080A in his adolescence.
He and his two children live in the Wilds of Northern British Columbia;
they're not lumberjacks, but they're OK.</EM>
<!-- *** END bio *** -->
<!-- *** BEGIN copyright *** -->
<hr>
<CENTER><SMALL><STRONG>
Copyright © 2002, Paul Evans.
Copying license <A HREF="../copying.html">http://www.linuxgazette.com/copying.html</A><BR>
Published in Issue 82 of <i>Linux Gazette</i>, September 2002</H5>
</STRONG></SMALL></CENTER>
<!-- *** END copyright *** -->
<HR>
<!--startcut ==========================================================-->
<CENTER>
<!-- *** BEGIN navbar *** -->
<IMG ALT="" SRC="../gx/navbar/left.jpg" WIDTH="14" HEIGHT="45" BORDER="0" ALIGN="bottom"><A HREF="lg_bytes.html"><IMG ALT="[ Prev ]" SRC="../gx/navbar/prev.jpg" WIDTH="16" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="index.html"><IMG ALT="[ Table of Contents ]" SRC="../gx/navbar/toc.jpg" WIDTH="220" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../index.html"><IMG ALT="[ Front Page ]" SRC="../gx/navbar/frontpage.jpg" WIDTH="137" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="http://www.linuxgazette.com/cgi-bin/talkback/all.py?site=LG&article=http://www.linuxgazette.com/issue83/evans.html"><IMG ALT="[ Talkback ]" SRC="../gx/navbar/talkback.jpg" WIDTH="121" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><A HREF="../lg_faq.html"><IMG ALT="[ FAQ ]" SRC="./../gx/navbar/faq.jpg"WIDTH="62" HEIGHT="45" BORDER="0" ALIGN="bottom"></A><A HREF="heriyanto.html"><IMG ALT="[ Next ]" SRC="../gx/navbar/next.jpg" WIDTH="15" HEIGHT="45" BORDER="0" ALIGN="bottom" ></A><IMG ALT="" SRC="../gx/navbar/right.jpg" WIDTH="15" HEIGHT="45" ALIGN="bottom">
<!-- *** END navbar *** -->
</CENTER>
</BODY></HTML>
<!--endcut ============================================================-->
|