#!/usr/bin/env ruby ### dict.rb --- RFC 2229 client for ruby. ## Copyright 2002,2003 by Dave Pearson ## $Revision: 1.9 $ ## ## dict.rb is free software distributed under the terms of the GNU General ## Public Licence, version 2. For details see the file COPYING. ### Commentary: ## ## The following code provides a set of RFC 2229 client classes for ruby. ## See for more details about dictd. ### TODO: ## ## o Add support for AUTH. # We need sockets. require "socket" ############################################################################ # Dictionary error class. class DictError < RuntimeError end ############################################################################ # Dict utility code. module Dict # Default host. DEFAULT_HOST = "localhost" # Default port. DEFAULT_PORT = 2628 # End of line marker. EOL = "\r\n" # End of data marker EOD = "." + EOL # The special database names. DB_FIRST = "!" DB_ALL = "*" # The guaranteed match strategies. MATCH_DEFAULT = "." MATCH_EXACT = "exact" MATCH_PREFIX = "prefix" # The various response numbers. RESPONSE_DATABASES_FOLLOW = 110 RESPONSE_STRATEGIES_FOLLOW = 111 RESPONSE_INFO_FOLLOWS = 112 RESPONSE_HELP_FOLLOWS = 113 RESPONSE_SERVER_INFO_FOLLOWS = 114 RESPONSE_DEFINITIONS_FOLLOW = 150 RESPONSE_DEFINITION_FOLLOWS = 151 RESPONSE_MATCHES_FOLLOW = 152 RESPONSE_CONNECTED = 220 RESPONSE_OK = 250 RESPONSE_NO_MATCH = 552 RESPONSE_NO_DATABASES = 554 RESPONSE_NO_STRATEGIES = 555 # Get the reply code of the passed text. def replyCode( text, default = nil ) if text =~ /^\d{3} / text.to_i elsif default default else raise DictError.new(), "Invalid reply from host \"#{text}\"." end end # replyCode should be private. private :replyCode end ############################################################################ # Dict base class. class DictBase # Mixin the Dict utility code. include Dict end ############################################################################ # Dictionary definition class. class DictDefinition < Array # Mixin the Dict utility code. include Dict # Constructor def initialize( details, conn ) # Call to the super. super() # Split the details out. details = /^\d{3} "(.*?)"\s+(\S+)\s+"(.*)"/.match( details ) @word = details[ 1 ] @database = details[ 2 ] @name = details[ 3 ] # Read in the definition. while ( reply = conn.readline() ) != EOD push( reply.chop() ) end end # Access to the word def word @word end # Access to the database def database @database end # Access to the database name def name @name end # Return an array of words you should also see in regard to this definition. def seeAlso join( " " ).scan( /\{(.*?)\}/ ) end end ############################################################################ # Dictionary definition list class. class DictDefinitionList < Array # Mixin the Dict utility code. include Dict # Constructor def initialize( conn ) # Call to the super. super() # While there's a definition to be had... while replyCode( reply = conn.readline() ) == RESPONSE_DEFINITION_FOLLOWS # ...add it to the list. push( DictDefinition.new( reply, conn ) ) end end end ############################################################################ # Base dictionary array class. class DictArray < Array # Mixin the Dict utility code. include Dict # Constructor def initialize( conn ) # Call to the super. super() # While there's a match to be had... while replyCode( reply = conn.readline(), 0 ) != RESPONSE_OK # ...add it to the list. push( reply ) if reply != EOD end end end ############################################################################ # Class for holding a dictionary item in a dictionary array. class DictArrayItem # Constructor. def initialize( text ) match = /^(\S+)\s+"(.*)"/.match( text ) @name = match[ 1 ] @description = match[ 2 ] end # Access to the name. def name @name end # Access to the description. def description @description end end ############################################################################ # Dictionary item array class. class DictItemArray < DictArray # Push the text as a DictArrayItem. def push( text ) super( DictArrayItem.new( text ) ) end end ############################################################################ # Dict client class. class DictClient < DictBase # Constructor. def initialize( host = DEFAULT_HOST, port = DEFAULT_PORT ) @host = host @port = port @conn = nil @banner = nil end # Read-only access to the host. def host @host end # Read-only access to the port. def port @port end # Are we connected? def connected? @conn != nil end # Check if there's a connected, throw an error if there isn't one. def checkConnection unless connected? raise DictError.new(), "Not connected." end end # checkConnection should be private. private :checkConnection # Send text to the server def send( text ) checkConnection() @conn.write( text + EOL ) end # send should be private. private :send # Connect to the host. def connect # Are we already connected? if connected? # Yes, throw an error. raise DictError.new(), "Attempt to connect a conencted client." else # Nope, open a connection @conn = TCPsocket.open( host, port ) # Get the banner. @banner = @conn.readline() # Valid return value? unless replyCode( @banner ) == RESPONSE_CONNECTED raise DictError.new(), "Connection refused \"#{@banner}\"." end # Now we announce ourselves to the server. send( "client org.davep.dict.rb $Revision: 1.9 $ " ) unless replyCode( reply = @conn.readline() ) == RESPONSE_OK raise DictError.new(), "Client announcement failed \"#{reply}\"" end # If we were passed a block, yield to it yield self if block_given? end end # Disconnect. def disconnect # Are we connected? if connected? # Yes, close the connection send( "quit" ) @conn.close() @conn = nil @banner = nil else # No, throw an error. raise DictError.new(), "Attempt to disconnect a disconnected client." end end # Return the banner we were handed. def banner checkConnection() @banner end # Core code for array oriented command. def arrayCommand( command, array_class, good, bad = nil ) # Send the command send( command ) # Worked? if replyCode( reply = @conn.readline() ) == good # Yes, load up the array array_class.new( @conn ) elsif bad and replyCode( reply ) == bad # "Bad" response, return an empty array Array.new() else # Something else, throw an error. raise DictError.new(), reply end end # arrayCommand is private. private :arrayCommand # Define a word. def define( word, database = DB_ALL ) arrayCommand( "define #{database} \"#{word}\"", DictDefinitionList, RESPONSE_DEFINITIONS_FOLLOW, RESPONSE_NO_MATCH ) end # Match a word. def match( word, strategy = MATCH_DEFAULT, database = DB_ALL ) arrayCommand( "match #{database} #{strategy} \"#{word}\"", DictItemArray, RESPONSE_MATCHES_FOLLOW, RESPONSE_NO_MATCH ) end # Get a list of available databases. def databases arrayCommand( "show db", DictItemArray, RESPONSE_DATABASES_FOLLOW, RESPONSE_NO_DATABASES ) end # Get a list of available strategies. def strategies arrayCommand( "show strat", DictItemArray, RESPONSE_STRATEGIES_FOLLOW, RESPONSE_NO_STRATEGIES ) end # Get the information for a given database. def info( database ) arrayCommand( "show info \"#{database}\"", DictArray, RESPONSE_INFO_FOLLOWS ) end # Get information about the server. def server arrayCommand( "show server", DictArray, RESPONSE_SERVER_INFO_FOLLOWS ) end # Get help from the server. def help arrayCommand( "help", DictArray, RESPONSE_HELP_FOLLOWS ) end end ############################################################################ # Provide a dict command. if $0 == __FILE__ # We're going to use long options. require "getoptlong" # Command result result = 1 # Setup the default parameters. $params = { :host => ENV[ "DICT_HOST" ] || Dict::DEFAULT_HOST, :port => ENV[ "DICT_PORT" ] || Dict::DEFAULT_PORT, :database => ENV[ "DICT_DB" ] || Dict::DB_ALL, :strategy => ENV[ "DICT_STRAT" ] || Dict::MATCH_DEFAULT, :match => false, :dbs => false, :strats => false, :serverhelp => false, :info => nil, :serverinfo => false, :help => false, :licence => false } # Print the help screen. def printHelp print "dict.rb v#{/(\d+\.\d+)/.match( '$Revision: 1.9 $' )[ 1 ]} Copyright 2002,2003 by Dave Pearson http://www.davep.org/ Supported command line options: -h --host Specify the host to be contacted (default is \"#{Dict::DEFAULT_HOST}\"). -p --port Specity the port to be connected (default is #{Dict::DEFAULT_PORT}). -d --database Specity the database to be searched (default is \"#{Dict::DB_ALL}\"). -m --match Perform a match instead of a define. -s --strategy Specity the strategy to use for the match/define (default is \"#{Dict::MATCH_DEFAULT}\"). -D --dbs List databases available on the server. -S --strats List stratagies available on the server. -H --serverhelp Display the server's help. -i --info Display information about a database. -I --serverinfo Display information about the server. --help Display this help. -L --licence Display the licence for this program. Supported environment variables: DICT_HOST Specify the host to be contacted. DICT_PORT Specify the port to be connected. DICT_DB Specify the database to be searched. DICT_STRAT Specify the strategy to use for the match/define. " end # Print the licence. def printLicence print "dict.rb - RFC 2229 client for ruby. Copyright (C) 2002,2003 Dave Pearson This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. " end # Get the arguments from the command line. begin GetoptLong.new().set_options( [ "--host", "-h", GetoptLong::REQUIRED_ARGUMENT ], [ "--port", "-p", GetoptLong::REQUIRED_ARGUMENT ], [ "--database", "-d", GetoptLong::REQUIRED_ARGUMENT ], [ "--match", "-m", GetoptLong::NO_ARGUMENT ], [ "--strategy", "-s", GetoptLong::REQUIRED_ARGUMENT ], [ "--dbs", "-D", GetoptLong::NO_ARGUMENT ], [ "--strats", "-S", GetoptLong::NO_ARGUMENT ], [ "--serverhelp", "-H", GetoptLong::NO_ARGUMENT ], [ "--info", "-i", GetoptLong::REQUIRED_ARGUMENT ], [ "--serverinfo", "-I", GetoptLong::NO_ARGUMENT ], [ "--help", GetoptLong::NO_ARGUMENT ], [ "--licence", "-L", GetoptLong::NO_ARGUMENT ] ).each {|name, value| $params[ name.gsub( /^--/, "" ).intern ] = value } rescue GetoptLong::Error printHelp() exit 1 end # Method for printing titles. def title( text, char ) print( ( char * 76 ) + "\n#{text}\n" + ( char * 76 ) + "\n" ) end # Method for printing a list. def printList( name, list ) title( "#{name} available on #{$params[ :host ]}:#{$params[ :port ]}", "=" ) list.each {|item| print item.class == DictArrayItem ? "#{item.name} - #{item.description}\n" : item } print "\n" end # The need for help overrides everything else if $params[ :help ] printHelp() result = 0 elsif $params[ :licence ] # As does the need for the legal mumbojumbo printLicence() result = 0 else begin # With a dict client... DictClient.new( $params[ :host ], $params[ :port ] ).connect() do |dc| # User wants to see a list of databases? printList( "Databases", dc.databases ) if $params[ :dbs ] # User wants to see a list of strategies? printList( "Strategies", dc.strategies ) if $params[ :strats ] # User wants to see the server help? printList( "Server help", dc.help ) if $params[ :serverhelp ] # User wants to see help on a database? printList( "Info for #{$params[ :info ]}", dc.info( $params[ :info ] ) ) if $params[ :info ] # User wants to see server information? printList( "Server information", dc.server ) if $params[ :serverinfo ] # Look up any words left on the command line. ARGV.each do |word| title( "Word: #{word}", "=" ) # Did the user require a match? if $params[ :match ] # Yes, display matches. if ( matches = dc.match( word, $params[ :strategy ], $params[ :database ] ) ).empty? print "No matches found\n" else matches.each {|wm| print "Database: \"#{wm.name}\" Match: \"#{wm.description}\"\n" } end else # No, display definitions. if ( defs = dc.define( word, $params[ :database ] ) ).empty? print "No definitions found\n" else defs.each do |wd| title( "From: #{wd.database} - #{wd.name}", "-" ) wd.each {|line| print line + "\n" } end end end end # Disconnect. dc.disconnect() end # If we made it this far everything should have worked. result = 0 rescue SocketError => e print "Error connecting to server: #{e}\n" rescue DictError => e print "Server error: #{e}\n" rescue /WIN/i.match( RUBY_PLATFORM ) ? Errno::E10061 : Errno::ECONNREFUSED => e print "Error connecting to server: #{e}\n" end end # Return the result to the caller. exit result end ### dict.rb ends here