Post #224,688
9/14/05 2:35:33 AM
|
Simple network programming language?
I just know this may turn into a flame war, but I'd appreciate getting the benefit of the collective experience/opinions/obsessions of the group.
Here's the sitch:
I'm teaching an introductory class in networking (TCP/IP) next term and my students fall into two distinct groups:
- Intermediate level programming students - reasonably comfortable with C++ and general code concepts. - E-business students - might have some basic VBscript stuff, but I'm not counting on even that.
SO I was thinking (mostly for the benefit of my coders):
In addition to the material on topologies, protocols and such, I'd like to have a fairly simple programming language/environment (scripting language a plus) that will allow my students to create very simple network code. Something along the lines of create a socket and send a few bytes of text across the room to another computer. Also it should run on Windows 2000/XP.
I'm thinking of this because no matter how much you stare at the OSI model and read about SNMP, there's something visceral about actually transmitting data using something you cobbled together yourself. A Marconi Moment, you might say.
So, fire away, as you wish. Maybe I'm totally out of my mind here, but I'd like to know what you folks think.
Tom Sinclair
Verence would rather cut his own leg off than put a witch in prison, since it'd save trouble in the long run and probably be less painful. -- (Terry Pratchett, Lords and Ladies)
|
Post #224,694
9/14/05 7:08:20 AM
|
Python.
That's my story and I am sticking to it.
Tons of things available, works on Windows, MAC, Linux, most (if not all) *NIX.
-- [link|mailto:greg@gregfolkert.net|greg], [link|http://www.iwethey.org/ed_curry|REMEMBER ED CURRY!] @ iwethey [image|http://www.danasoft.com/vipersig.jpg||||]
|
Post #224,696
9/14/05 8:11:56 AM
|
What Greg said.
A network server in Python is a few lines of easily-read code.
[link|http://www.complete.org/publications/pynet/|Book] (that I haven't read, caveat emptor)
[link|http://www.amk.ca/files/simple/fingerd.py|Example server] - finger daemon in 14 lines of code
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #224,705
9/14/05 9:52:32 AM
|
That's exactly what I was thinking
It's simple, cross-platform and has tons of libraries.
I guess I just need links to some good tutorial material and you've given me a good start.
On a further note, does doing this seem like a good idea, given the audience?
Personally I'm of the opinion that if you're going to be a 'computer professional', you should know at least know a little scripting.
Tom Sinclair
The chieftain had been turned into a pumpkin although, in accordance with the rules of universal humour, he still had his hat on. -- (Terry Pratchett, Lords and Ladies)
|
Post #224,952
9/16/05 1:57:05 AM
|
cautions, cause..
I can't spell caveates
The business guys will think they're programmers and the programmers will think they have marketable experience. a little expectation setting is in order. There's a whole lot of layers between the keyboard and the other side of the room. explore how many they can find. :-)
Definitely a good idea on the whole.
Have fun, Carl Forde
|
Post #224,993
9/16/05 11:39:51 AM
|
Understood
I definitely realize it's a tricky balance and I have to carefully and clearly manage expectations.
Fortunately, that's a normal part of my job.
Another possibility I considered was using a combination of tools such as netcat, ethereal, nemesis and nmap to let them fiddle with packets.
Tom Sinclair
Cuddy had only been a guard for a few days, but already he had absorbed one important and basic fact: it is almost impossible for anyone to be in a street without breaking the law. -- (Terry Pratchett, Men at Arms)
|
Post #224,717
9/14/05 10:28:29 AM
|
REBOL gets you there the fastest
But then its networking is at such a level that it probably won't teach the lesson that you are trying to convey.
I'd recommend Python as well. Java's not too shabby when it comes to the networking portion, but you'd have to teach a lot of the language before you can get to the matter at hand.
|
Post #224,720
9/14/05 10:39:38 AM
|
Agreed.
Python is a great teaching language. It reads like pseudocode, it's not *too* high-level, and it's powerful enough to do just about anything.
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #224,725
9/14/05 10:47:59 AM
|
I considered Java briefly
because I really like the java.net API.
Now, if the class were only my programmers (familiar with C++), I probably would have gone with Java.
With my e-biz folks, though, python or something similar seemed like a better bet.
I can give them the basics fairly quickly and for the networking stuff hand out some sample code.
Tom Sinclair
In fact, the mere act of opening the box will determine the state of the cat, although in this case there were three determinate states the cat could be in: these being Alive, Dead, and Bloody Furious. -- Schrodinger's Moggy explained (Terry Pratchett, Lords and Ladies)
|
Post #224,797
9/14/05 4:20:32 PM
|
Definietly not Java or anything with a compiler
they'll get lost in the build process and miss the lesson you are trying to teach.
"Whenever you find you are on the side of the majority, it is time to pause and reflect" --Mark Twain
"The significant problems we face cannot be solved at the same level of thinking we were at when we created them." --Albert Einstein
"This is still a dangerous world. It's a world of madmen and uncertainty and potential mental losses." --George W. Bush
|
Post #224,803
9/14/05 4:32:41 PM
|
Granted
I only considered it for brief moment.
A scripting language is much better for this scenario since they can make changes and test right away.
So far it looks like my initial choice of python is holding up, but I'm still open to other options if anyone has one.
Tom Sinclair
"Not a man to mince words. People, yes. But not words." -- (Terry Pratchett, Small Gods)
|
Post #224,746
9/14/05 1:12:53 PM
|
perl, creating sockets and listening is fairly easy
"the reason people don't buy conspiracy theories is that they think conspiracy means everyone is on the same program. Thats not how it works. Everybody has a different program. They just all want the same guy dead. Socrates was a gadfly, but I bet he took time out to screw somebodies wife" Gus Vitelli
Any opinions expressed by me are mine alone, posted from my home computer, on my own time as a free american and do not reflect the opinions of any person or company that I have had professional relations with in the past 49 years. meep questions, help? [link|mailto:pappas@catholic.org|email pappas at catholic.org]
|
Post #224,804
9/14/05 4:34:02 PM
|
Thanks for the tip
(Heads off to his Safari Bookshelf....)
Tom Sinclair
Of course he was all in favour of Armageddon in *general* terms. -- (Terry Pratchett & Neil Gaiman, Good Omens)
|
Post #224,822
9/15/05 1:49:10 PM
|
But not as readable as Python.
perl isn't a beginner's language, IMO.
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #224,862
9/15/05 5:32:30 PM
|
This isn't readable?
\n#!/usr/bin/perl -w\nuse strict;\n\nuse IO::Socket qw(:DEFAULT :crlf);\n\nmy $host = shift;\nmy $port = shift;\nmy $string = shift;\nunless (defined($string)){\n die "Must supply host, port, and text string to send!\\n";\n}\n\nprint "[$host]:[$port]:[$string]\\n";\n\nmy $sock = IO::Socket::INET->new("$host:$port")\n or die "Can't connect to $host:$port: $!\\n";\n\n\nmy($rc);\n$rc = printf $sock ("$string\\n");\nif (!$rc) {\n printf STDERR ("Can't write, client connection error %s", $sock->error());\n $sock->close();\n exit(1);\n}\nmy($line);\nif (!defined($line = $sock->getline())) {\n printf STDERR ("Can't read, client connection error %s", $sock->error());\n $sock->close();\n exit(1);\n}\n$sock->close();\n\nprint $line;\nexit(0);\n
|
Post #224,864
9/15/05 5:37:04 PM
|
Ben could reduce that to about 12 characters... ;-)
|
Post #224,866
9/15/05 5:39:02 PM
|
Of course he could
And we'd all ooooh and aaaah.
And then 2 years from now, the junior maintainance programmer would hunt him down and kill him.
|
Post #224,869
9/15/05 5:45:23 PM
|
:-) Yup, but in Python one can't do that.
Apparent line noise isn't significant information in Python.
Cheers, Scott.
|
Post #224,871
9/15/05 5:50:29 PM
|
Hmm
Before this degenerates into a language war, something is missing.
COMPETING CODE!
Until posted, the evangelists of nothing to say about it.
|
Post #224,872
9/15/05 6:00:41 PM
|
Write That Code!
Contestant: "Tom, I can Write That Code in 15 Characters!"
Audience:Whooo!
Tom Kennedy: "Write That Code!"
:-D
Cheers, Scott.
|
Post #225,517
9/19/05 6:10:12 PM
|
Sorry I'm late to the Party ... Ruby Version
As Ben suggested, Ruby is not a bad choice either. \n#!/bin/env ruby\n\nrequire 'socket'\n\nhost, port, string = ARGV\nfail "Must supply host, port, and text string to send!" unless string\n\nputs "#{host}:#{port}:#{string}"\n\nTCPSocket.open(host, port) do |sock|\n sock.puts(string)\n puts sock.gets\nend\n And the low level protocols are there too, if you wish to work at the bind/connect level.
-- -- Jim Weirich jim@weirichhouse.org [link|http://onestepback.org|http://onestepback.org] --------------------------------------------------------------------- "Beware of bugs in the above code; I have only proved it correct, not tried it." -- Donald Knuth (in a memo to Peter van Emde Boas)
|
Post #225,518
9/19/05 6:19:26 PM
|
Where's the error handling?
For the curious... and since the other examples had it. :-)
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #225,526
9/19/05 6:39:45 PM
|
I wouldn't bet that it is missing
I don't know that library, but a common Ruby idiom is that if you pass a block to the open method, then the called method is responsible for doing the open, loop, and necessary error checks. :-)
Cheers, Ben
I have come to believe that idealism without discipline is a quick road to disaster, while discipline without idealism is pointless. -- Aaron Ward (my brother)
|
Post #225,529
9/19/05 7:11:36 PM
|
OK, fine... but
Where is the error handling? :-)
Unless the library is just eating the error (bad), then the code is ignoring the error (which is what I'm asking about).
In other words, how will it know if the connection fails?
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #225,531
9/19/05 7:25:13 PM
|
As I said...
If it follows the idiom that I've seen in other libraries, the library throws an exception that will display default error messages which will suffice for simple scripts.
Cheers, Ben
I have come to believe that idealism without discipline is a quick road to disaster, while discipline without idealism is pointless. -- Aaron Ward (my brother)
|
Post #225,532
9/19/05 7:29:04 PM
|
Near enough to Python, then.
But for purposes of this example, that's not what's being done.
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #225,540
9/19/05 8:42:11 PM
|
Re: Near enough to Python, then.
But for purposes of this example, that's not what's being done.Actually, it is. The Perl and Python examples (1) close the socket (if it is open), (2) print a simple error message, (3) and exit with an error code. The Ruby version handles (1) within the open method automatically, and (2) and (3) in the top level exception handler provided by Ruby. If you want a more custom error message than is provided by the default error handler, or if you want a more specific error code on exit, you could do: \nbegin\n TCPSocket.open(host, port) do |sock|\n sock.puts(string)\n puts sock.gets\n end\nrescue Exception => ex\n puts "Error: #{ex} at #{ex.backtrace[0]}"\n exit(1)\nend\n Note that it is not necessary to explicitly close the socket, even in the presence of exceptions, when using the block form of open as coded in the examples. Error output from original code: \nrnet.rb:10:in `initialize': Connection refused - connect(2) (Errno::ECONNREFUSED)\n\tfrom rnet.rb:10:in `open'\n\tfrom rnet.rb:10\n Error output from the modified code above: \nError: Connection refused - connect(2) at rnet2.rb:11:in `initialize'\n
-- -- Jim Weirich jim@weirichhouse.org [link|http://onestepback.org|http://onestepback.org] --------------------------------------------------------------------- "Beware of bugs in the above code; I have only proved it correct, not tried it." -- Donald Knuth (in a memo to Peter van Emde Boas)
|
Post #225,543
9/19/05 8:52:02 PM
9/19/05 8:53:23 PM
|
As I said...
The Python example does pretty much the same thing.
If you leave off the exception handling, it closes the socket, prints a simple error message, and exits with an error code. There's nothing magical in the Ruby example. :-)
So again, the examples being given here DO have a more custom error message. Which was all I was asking for. The point of the examples being good, explicit (as in, you can expect errors here, etc.) networking code suitable for showing to a beginner.
So, thanks for the example. :-)
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
Edited by admin
Sept. 19, 2005, 08:53:23 PM EDT
|
Post #225,544
9/19/05 8:57:07 PM
|
Ahh, makes me long for the old days
NOT!
Doing IO programming (serial or parallel), bit twiddling the IOCTL calls for every byte, checking the handshake lines, etc.
Man that sucked!
|
Post #224,881
9/15/05 6:46:06 PM
9/15/05 6:47:23 PM
|
Oops.
|
Post #224,883
9/15/05 6:47:12 PM
|
ICJRLPD (new thread)
Created as new thread #224882 titled [link|/forums/render/content/show?contentid=224882|ICJRLPD]
When somebody asks you to trade your security for freedom, it isn't your freedom they're talking about.
|
Post #224,874
9/15/05 6:14:58 PM
|
As I said, "not AS readable as the Python"
Perl is a difficult language for newbies. #!/usr/bin/python\n\nimport sys, socket, telnetlib\n\nif (len(sys.argv) < 4):\n sys.exit('Must supply host, port, and text string to send!\\n')\n\n(host, port, string) = sys.argv[1:]\n\nprint '%s:%s Sending: %s\\n' % (host, port, string)\n\nconn = telnetlib.Telnet()\ntry:\n conn.open(host, port)\n conn.write('%s\\n' % (string))\n print conn.read_all()\n conn.close()\n\nexcept socket.error, (errno, errstr):\n conn.close()\n sys.exit("Error: %s!" % (errstr))\n You're also a seasoned Perl programmer. The code that a new Perl programmer can easily create is best-described as "write only". Working Python code is nearly impossible to obfuscate without getting into some extremely esoteric constructs.
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #224,877
9/15/05 6:24:35 PM
|
Not bad
I like the exception handling.
|
Post #225,105
9/16/05 6:15:51 PM
|
What. He. Said.
Tht does it...I gotta go find a Python book somewhere...(Uhh, Scott...what animal does the Python book have on it? No, I don't believe the answer is that easy....)
jb4 shrub●bish (Am., from shrub + rubbish, after the derisive name for America's 43 president; 2003) n. 1. a form of nonsensical political doubletalk wherein the speaker attempts to defend the indefensible by lying, obfuscation, or otherwise misstating the facts; GIBBERISH. 2. any of a collection of utterances from America's putative 43rd president. cf. BULLSHIT
|
Post #225,107
9/16/05 6:25:59 PM
9/16/05 6:27:38 PM
|
WTF do YOU think it has on it...?
Sheesh. Yes, it IS that easy.
[link|http://www.oreilly.com/catalog/pythonian/|http://www.oreilly.c...atalog/pythonian/]
Now, the "Learning Python" book is a little different... it has prey on the cover: [link|http://www.oreilly.com/catalog/lpython2/|http://www.oreilly.com/catalog/lpython2/]
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
Edited by admin
Sept. 16, 2005, 06:27:38 PM EDT
|
Post #225,109
9/16/05 6:29:52 PM
|
I don't think you'll be surprised. (img)
[image|ftp://ftp.ora.com/pub/graphics/book_covers/hi-res/0596000855.jpg|0|Programming Python|360|250]
There are other books that have rats and stuff on the cover.
Cheers, Scott.
|
Post #225,113
9/16/05 6:47:12 PM
|
Hiss
The Programming Python book is large and rather difficult to read sequentially. Pretty good as a reference when you want to learn some aspects on a particular subject, but it is not really a book for learning how to program in Python. The Learning Python book is a good sequential read, but lacks a whole lot in terms of breadth. Best read Learning first, and then use Programming as a reference.
Or you could do like admin and just use the freely available web resources to learn the language.
|
Post #225,425
9/19/05 7:02:40 AM
|
Wel, it's beautiful code
But won't it turn a course on TCP/IP into a course on Python networking libraries? The question is, what's the goal of the course? Is it to be able to do things on the net, or to understand how the net realy works?
------
179. I will not outsource core functions. -- [link|http://omega.med.yale.edu/~pcy5/misc/overlord2.htm|.]
|
Post #225,430
9/19/05 7:38:22 AM
|
Use the socket libraries then.
Readable code is more about structure and obviousness than libraries.
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #225,442
9/19/05 10:33:04 AM
|
It's introduction to networking
but I have programmers in the class.
The idea was to give them just enough to shoot a few bytes across a LAN.
Give them a little Marconi Moment, as it were.
Another possibility is to show them some network hacking with nmap, Metasploit et al.
Tom Sinclair
The place looked as though it had been visited by Gengiz Cohen [footnote: hence the term "wholesale destruction"]. -- (Terry Pratchett, Lords and Ladies)
|
Post #225,444
9/19/05 10:52:57 AM
|
I'd go with socket basics.
It's not that difficult conceptually, and will make How It All Works much clearer than any other thing you can do. After all, once you get the connection made and can pass text back and forth, creating a service is just all about how it's processed at each end.
To my mind, I'd start with the socket basics, keep it quite basic, and point them at further info if they wish to follow up on their own. After seeing how sockets work, showing them the kinds of things you can do with nmap etc will be much much clearer to them. Start down near the root, and move the course further up the tree as it progresses.
--\n-------------------------------------------------------------------\n* Jack Troughton jake at consultron.ca *\n* [link|http://consultron.ca|http://consultron.ca] [link|irc://irc.ecomstation.ca|irc://irc.ecomstation.ca] *\n* Kingston Ontario Canada [link|news://news.consultron.ca|news://news.consultron.ca] *\n-------------------------------------------------------------------
|
Post #225,451
9/19/05 11:23:15 AM
|
Re: I'd go with socket basics.
Server: #!/usr/bin/python\n\nimport sys, socket\n\nif (len(sys.argv) < 2):\n sys.exit('Must supply port!\\n')\n\nport = sys.argv[1]\nhost = socket.gethostbyname(socket.gethostname())\n\nprint '%s:%s Listening:\\n' % (host, port)\n\nsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\ntry:\n sock.bind((host, int(port)))\n sock.listen(1)\n\n conn, addr = sock.accept()\n print 'Connection from: ' + str(addr)\n\n str = conn.recv(1024)\n print 'Echoing "%s"' % (str)\n conn.send(str)\n\n conn.close()\n\nexcept socket.error, (errno, errstr):\n conn.close()\n sys.exit("Error: %s!" % (errstr)) Client: #!/usr/bin/python\n\nimport sys, socket\n\nif (len(sys.argv) < 4):\n sys.exit('Must supply host, port, and text string to send!\\n')\n\n(host, port, string) = sys.argv[1:]\n\nprint '%s:%s Sending: %s\\n' % (host, port, string)\n\nsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\ntry:\n sock.connect((host, int(port)))\n sock.sendall('%s\\n' % (string))\n recv = ''\n while 1:\n str = sock.recv(1024)\n if not str: break\n recv += str\n\nexcept socket.error, (errno, errstr):\n sock.close()\n sys.exit("Error: %s!" % (errstr))\n\nprint "Received:\\n" + recv
Regards,
-scott anderson
"Welcome to Rivendell, Mr. Anderson..."
|
Post #225,458
9/19/05 12:50:17 PM
|
Didn't say it would take very long
:)
Of course, for the students that DON'T know programming, learning that will take quite a bit longer; OTOH, once they DO grok it, they will be much better positioned to correctly grok computers in general.
There are a LOT of users that have very, ummm, odd intellectual models of what's actually going on in there.
Cue Tom Waits as appropriate.
--\n-------------------------------------------------------------------\n* Jack Troughton jake at consultron.ca *\n* [link|http://consultron.ca|http://consultron.ca] [link|irc://irc.ecomstation.ca|irc://irc.ecomstation.ca] *\n* Kingston Ontario Canada [link|news://news.consultron.ca|news://news.consultron.ca] *\n-------------------------------------------------------------------
|
Post #225,461
9/19/05 1:02:31 PM
|
Programming Languages will influence the model
So one must be careful to choose a PL that has the proper level of abstraction for the lesson to be taught and the model to be molded. If you want them to understand it in terms of bits and bytes and machine instructions, C and it's ilk is the way to go. If you want them to understand it as a set of libraries that can be slowly dissected, then Python and similar languages can be the way to go.
Either way, both are really just models for the more abstract thought of network transport.
|
Post #225,468
9/19/05 1:29:34 PM
|
Indeed
and I think a higher level view than C is the one for this. I might get a kick out of bit hacking from time to time, but I'm funny and strange. The key concept is two apps, one with a listen socket and one connecting to it, exchanging data through a series of read and write operations to their end of the socket. Once they grok that, then things like the output (in OS/2) of iptrace && ipformat get a lot easier to figure out.
--\n-------------------------------------------------------------------\n* Jack Troughton jake at consultron.ca *\n* [link|http://consultron.ca|http://consultron.ca] [link|irc://irc.ecomstation.ca|irc://irc.ecomstation.ca] *\n* Kingston Ontario Canada [link|news://news.consultron.ca|news://news.consultron.ca] *\n-------------------------------------------------------------------
|
Post #225,481
9/19/05 3:03:14 PM
|
Definitely want high level languages
Keep this at the upper levels of the TCP/IP stack.
Tom Sinclair
Crowley was in Hell's bad books. Not that Hell has any other kind. -- (Terry Pratchett & Neil Gaiman, Good Omens)
|
Post #225,538
9/19/05 8:22:01 PM
|
Yep.
------
179. I will not outsource core functions. -- [link|http://omega.med.yale.edu/~pcy5/misc/overlord2.htm|.]
|
Post #224,888
9/15/05 7:05:48 PM
|
I'd suggest Ruby...
but I'd actually be comfortable with most of the basic scripting languages.
The real question is what you're planning to use for a text. If you find a text you like, and it does examples in a half-way reasonable language, then I'd use that language.
If this was a slightly more advanced group, I might suggest [link|http://www.modperl.com/perl_networking/|Network Programming with Perl] as a reference, but I suspect that that's more in depth than you want to go.
Cheers, Ben
I have come to believe that idealism without discipline is a quick road to disaster, while discipline without idealism is pointless. -- Aaron Ward (my brother)
|
Post #224,891
9/15/05 7:08:36 PM
|
I believe I stole that example from it
a couple of weeks ago when I needed to test a little server I had whipped up.
|
Post #224,944
9/16/05 1:18:42 AM
|
I'm thinking of quick and dirty HOWTO here
The course itself is an introduction to networking technology and concepts.
In other words, a few hours of instruction with practice (our class periods are four hours long) and then a quick little client/server to send a string or two across the LAN.
Nothing fancy, but perhaps I'm being unrealistic.
Tom Sinclair
"[...] a number of offences of murder by means of a blunt instrument, to whit, a dragon, and many further offences of generalized abetting [...]" -- (Terry Pratchett, Guards! Guards!)
|
Post #225,013
9/16/05 1:06:31 PM
|
Consider Object REXX
It is a scripting language, and best of all you can show both imperative and object-oriented approaches to socket programming, as the libraries let you either essentially use the C api (RxSock) or to create more high level object oriented code (don't remember the lib name off the top of my head). Also, it's very readable; once you create the socket and redirect I/O to it and/or create an object handle for it, sending and receiving text is as simple as:
say "here's the message" pull theAnswer
in the imperative form, and
mySocket~lineout("here's the message") theAnswer = mySocket~linein
in the OO form.
--\n-------------------------------------------------------------------\n* Jack Troughton jake at consultron.ca *\n* [link|http://consultron.ca|http://consultron.ca] [link|irc://irc.ecomstation.ca|irc://irc.ecomstation.ca] *\n* Kingston Ontario Canada [link|news://news.consultron.ca|news://news.consultron.ca] *\n-------------------------------------------------------------------
|
Post #225,093
9/16/05 5:16:33 PM
|
Thanks for the reminder
I loved Rexx when I used it on my Amiga. Very simple and straightforward to use and I used it to automate a ton of tasks.
Tom Sinclair
"Did I hear things, or can that little dog speak?" said Dibbler. "He says he can't," said Victor. Dibbler hesitated. "Well," he said, "I suppose he should know." -- Dibbler meets Gaspode the Wonder Dog (Terry Pratchett, Moving Pictures)
|