Next / Previous / TCC home / NMT home

Source listing for pinemerge.py

logo
#!/usr/local/bin/python
#--
"""pinemerge.py  $Revision: 1.7 $  $Date: 1997/09/30 23:09:17 $

Author:
    John W. Shipman (john@nmt.edu), New Mexico Tech
    Computer Center, Socorro, NM 87801.  This program is in the
    public domain.

Purpose:

    Merges two or more Pine address books.

    Command line arguments:

        pinemerge file1 file2 ...

    A merged address book is written to stdout.  Errors may be
    written to stderr.

    For any address that whose nickname is in multiple files but
    is not the same in all other fields, the first-named file
    wins, and information about the winners and losers is written
    to stderr.

References:
    The Pine address book format is document at the Pine
    Information Center, http://www.washington.edu/pine/.  Refer
    to the ``Technical Notes'' page, more specifically:

        http://www.washington.edu/pine/tech-notes/low-level.html#addrbook

    This software includes intended functions for Cleanroom
    verification.  For more information on this methodology,
    see http://www.nmt.edu/~shipman/soft/clean/.
"""

import sys, string, types


# - - - - -   C l a s s   A d d r B o o k   - - - - -

class AddrBook:
    """Address book object

        Each instance represents one complete Pine address book.

        Exports:
            AddrBook ( fileName )
                [ if fileName is None ->
                    return a new, empty AddrBook object
                  if fileName is a string ->
                    if fileName names a readable, valid Pine address
                    book ->
                      return a new AddrBook object representing
                      the entries from that file
                    else ->
                      <stderr>  :=  error message(s)
                      raise IOError
                ]
            .insert ( a )
                [ if a is an Address object ->
                    if there is no entry for a's nickname in self ->
                      self  :=  self with a added
                    else ->
                      raise ValueError
                ]
            .write ( outFile )
                [ if outFile is a writeable file ->
                    outFile  +:=  self in Pine address book format,
                                  with entries in ascending order
                                  by (full name+nickname)
                ]
            .nickMap ==
              a dictionary mapping each nickname to the corresponding
              Address object
    """

# - - -   _ _ i n i t _ _   - - -

    def __init__ ( self, fileName=None ):
        """Constructor function for AddrBook
        """
        #-- 1 --
        self.nickMap  =  { }

        #-- 2 --
        #-[ if fileName is None -> I
        #   else ->
        #     if fileName names a readable, valid Pine address book ->
        #       self.nickMap  :=  entries mapping each nickname to the
        #                         corresponding Address object
        #     else ->
        #       sys.stderr  +:=  error message
        #       raise IOError
        #-]
        if fileName is not None:
            self.__readFile ( fileName )


# - - -   i n s e r t   - - -

    def insert ( self, address ):
        if self.nickMap.has_key ( address.nickname ):
            raise ValueError
        else:
            self.nickMap[address.nickname]  =  address


# - - -   w r i t e   - - -

    def write ( self, outFile ):
        #-- 1 --
        #-[ L  :=  a list of all the Address objects in self
        #-]
        L = self.nickMap.values ( )

        #-- 2 --
        #-[ L  :=  L, sorted by (fullName+nickname)
        #-]
        L.sort()

        #-- 3 --
        #-[ outFile  +:=  the elements of L as rendered by their str() methods
        #-]
        for a in L:
            outFile.write ( str ( a ) + "\n" )


# - - -   _ _ r e a d F i l e   - - -

    def __readFile ( self, fileName ):
        """Function to read a Pine address book.
            [ if fileName is a string ->
                if fileName names a readable, valid Pine address book ->
                  self.nickMap  :=  entries mapping each nickname to the
                                    corresponding Address object
                else ->
                  sys.stderr  +:=  error message
                  raise IOError
            ]
        """
        #-- 1 --
        #-[ if fileName can be opened for reading ->
        #     inFile  :=  fileName opened for reading
        #   else ->
        #     sys.stderr  +:=  error message
        #     raise IOError
        #-]
        try:
            inFile  =  open ( fileName, "r" )
        except:
            sys.stderr.write ( "Can't open file `" + fileName +
                               "' for reading.\n" )
            raise IOError, "Can't open Pine address book."

        #-- 2 --
        #-[ L  :=  a list of the lines in file inFile, with line
        #          terminators intact, and continuation lines
        #          (those that start with one or more spaces)
        #          attached to previous lines
        #-]
        L  =  self.__unfoldInput ( inFile )

        #-- 3 --
        #-[ if L represents a valid Pine address book as a list
        #   of lines ->
        #     self.nickMap  +:=  entries mapping each nickname from L
        #                        to the corresponding Address object
        #   else ->
        #     sys.stderr  +:=  error message(s)
        #     raise IOError
        #-]
        self.__processList ( L )

        #-- 4 --
        inFile.close ( )
        return self

# - - -   _ _ u n f o l d I n p u t   - - -

    def __unfoldInput ( self, inFile ):
        """Reads and ``unfolds'' input.
            [ if inFile is a readable file object ->
                return the contents of that file as a list of lines,
                with continuation lines (those that start with one or
                more spaces) reassembled to make one long line for
                each entry
            ]
        """
        #-- 1 --
        #-[ foldedList  :=  the lines of inFile represented as a list
        #                   of strings with their line terminators intact
        #-]
        foldedList  =  inFile.readlines ( )

        #-- 2 --
        result  =  []

        #-- 3 --
        #-[ result  +:=  the lines of foldedList, with continuations
        #                reassembled and all line terminators removed
        #-]
        while  len ( foldedList ) > 0:
            #-- 3 body --
            #-[ foldedList  :=   foldedList with its first line removed
            #                    and all following lines that start with
            #                    at least one space
            #   result      +:=  contents of the first line from foldedList,
            #                    without its line terminator, followed by
            #                    the contents of any continuations trimmed
            #                    of their leading spaces and trailing line
            #                    terminators
            #-]
            result.append ( self.__trimGroup ( foldedList ) )

        #-- 4 --
        return result


# - - -   _ _ t r i m G r o u p   - - -

    def __trimGroup ( self, L ):
        """Method to remove the next line from L with its continuations.
            [ if L is a nonempty list of strings ->
                L  :=  L with its first element removed and all following
                       lines that start with at least one space
                return a string containing the first element of L without
                its line terminator, followed by the contents of any
                continuations trimmed of their leading spaces and trailing
                line terminators
            ]
        """
        #-- 1 --
        #-[ result  :=  the first element of L with its line terminator
        #               removed     
        #   L       :=  L with its first element removed
        #-]
        result  =  self.__termStrip(L[0])
        del L[0]

        #-- 2 --
        #-[ if L begins with a line that starts with a space ->
        #     result  +:=  concatenation of all leading lines from L that
        #                  start with spaces, but with leading spaces
        #                  and line terminators removed
        #   else -> I
        #-]
        while ( ( len ( L ) > 0 ) and
                ( len ( L[0] ) > 0 ) and
                ( L[0][0] == " " ) ):
            #-- 2 loop --
            #-[ result  +:=  contents of L[0] without leading spaces or
            #                line terminators
            #-] L       :=   L with its first element removed
            result  =  result + string.lstrip ( self.__termStrip ( L[0] ) )
            del L[0]

        #-- 3 --
        return result


# - - -   _ _ t e r m S t r i p   - - -

    def __termStrip ( self, S ):
        """Remove the line terminator from S.
            [ if S is a string ->
                if S ends with a linefeed ->
                  return S without its last character
                else ->
                  return S
            ]
        """
        if ( ( len ( S ) > 0 ) and
             ( S[-1] == "\n" ) ):
            return S[:-1]
        else:
            return S


# - - -   _ _ p r o c e s s L i s t   - - -

    def __processList ( self, L ):
        """Parse the lines of a Pine address book.
            [ if L is the address book as a list of long (not folded)
              lines ->
                if L represents a valid Pine address book ->
                  self.nickMap  +:=  entries mapping each nickname
                                     from L to the corresponding
                                     Address object
                else ->
                  sys.stderr  +:=  error message(s)
                  raise IOError
            ]
        """
        #-- 1 --
        anyError  =  0

        #-- 2 --
        #-[ if L contains no errors ->
        #     self.nickMap  +:=  entries mapping nicknames from L to
        #                        the corresponding Address objects
        #   else ->
        #     self.nickMap  +:=  entries mapping valid nicknames from L to
        #                        the corresponding Address objects
        #     sys.stderr    +:=  error message(s)
        #     anyError      :=   1
        #-]
        for line in L:
            #-- 2 loop --
            #-[ if line contains an error ->
            #     sys.stderr  +:=  error message(s)
            #     anyError    :=   1
            #   else ->
            #     self.nickMap  +:=  an entry mapping the nickname from
            #                        line to an Address object representing
            #                        line
            #-]
            anyError  =  anyError or self.__parseAddressLine ( line )

        #-- 3 --
        if anyError:
            raise IOError, "Errors in the address book"


# - - -   _ _ p a r s e A d d r e s s L i n e   - - -

    def __parseAddressLine ( self, line ):
        """Parses one address book entry (as a reunified, long line).
            [ if line is a valid address line in the context of self ->
                self.nickMap  +:=  an entry mapping the nickname from
                                   line to an Address object representing
                                   that line
                return 0
              else ->
                sys.stderr  +:=  error message(s)
                return 1
            ]
        """
        #-- 1 --
        #-[ fields  :=  line, split on TAB characters
        #-]
        fields  =  string.split ( line, "\t" )

        #-- 2 --
        #-[ if fields has 3-5 parts ->
        #     nickname  :=  fields[0]
        #     fullName  :=  fields[1]
        #     email     :=  fields[2]
        #     fcc       :=  fields[3], or None if len(fields)<4
        #     comment   :=  fields[4], or None if len(fields)<5
        #   else ->
        #     sys.stderr  +:=  error message
        #     return 1
        #-]
        if  ( 3 <= len ( fields ) <= 5 ):
            nickname  =  fields[0]
            fullName  =  fields[1]
            email     =  fields[2]
            fcc       =  None
            comment   =  None
            if len(fields) > 3:
                fcc  =  fields[3]
                if len(fields) > 4:
                    comment  =  fields[4]

        else:
            sys.stderr.write ( "*** Lines should have 3-5 fields:\n" +
                               line + "\n" )
            return 1

        #-- 3 --
        address  =  Address ( nickname, fullName, email, fcc, comment )

        #-- 4 --
        #-[ if self.nickMap has a key (nickname) ->
        #     sys.stderr  +:=  error message
        #     return 1
        #   else -> I
        #-]
        if  self.nickMap.has_key ( nickname ):
            sys.stderr.write ( "*** Duplicate nickname:\n" +
                               line + "\n" )
            sys.stderr.write ( "*** Other nickname is for " +
                               self.nickMap[nickname].fullName + "\n" )
            return 1

        #-- 5 --
        self.nickMap[nickname]  =  address
        return 0


# - - - - -   C l a s s   A d d r e s s   - - - - -

class Address:
    """Address book entry object

        Each instance represents one entry in a Pine address book.

        Exports:
          Address ( nickname, fullName, address, fcc, comments )
            [ if (nickname is the nickname as a string without tabs)
              and (fullName is the full name as a string without tabs)
              and (address is the e-mail address as a string without tabs)
              and (fcc is the file-copy-to attribute as a string without
              tabs, or None)
              and (comments is a comments string without tabs, or None) ->
                return a new Address object with those attributes
            ]
          .show ( )
            [ returns a string containing the fields of self in
              a human-readable format
            ]
          .nickname     [ The nickname as a string ]
          .fullName     [ The full name as a string ]
          .address      [ The e-mail address as a string ]
          .fcc          [ The fcc attribute as a string, or None ]
          .comments     [ The comments as a string, or None ]
          .__str__()
            [ self, represented as a list of lines wrapped as per the
              Pine addressbook format, each line ending with newline
            ]
          .__cmp__()
            [ return cmp() value, using fullName as the major key
              and nickname as the minor key
            ]
    """

# - - -   _ _ i n i t _ _   - - -

    def __init__ ( self, nickname, fullName, address, fcc, comments ):
        """Constructor for Address
        """
        self.nickname  =  nickname
        self.fullName  =  fullName
        self.address   =  address
        self.fcc       =  fcc
        self.comments  =  comments


# - - -   . s h o w   - - -

    def show ( self ):
        return ( "%s (%s) %s" %
                 ( self.nickname, self.fullName, self.address ) )


# - - -   _ _ s t r _ _   - - -

    def __str__ ( self ):
        """Translate self to external form.
            [ return self as a list of strings, each ending with newline,
              such that the one-long-line form of self is broken on
              TABS, or commas in a distribution list
            ]
        """
        #-- 1 --
        outList  =  []

        #-- 2 --
        #-[ outList  :=  outList with self's nickname added
        #-]
        self.__addField ( outList, self.nickname + "\t" )

        #-- 3 --
        #-[ outList  +:=  TAB + self's fullname
        #-]
        self.__addField ( outList, self.fullName + "\t" )

        #-- 4 --
        #-[ outList  :=  outList with self's address added, followed by a TAB,
        #               and breaking address lists on commas if necessary
        #-]
        self.__breakAddress ( outList, self.address )
        
        #-- 5 --
        #-[ outList  :=  outList with self's fcc added, if any, followed by a
        #               TAB
        #-]
        if self.fcc is not None:
            self.__addField ( outList, "\t" + self.fcc )

        #-- 6 --
        #-[ outList  :=  outList with self's comments added, if any,
        #-]
        if self.comments is not None:
            self.__addField ( outList, "\t" + self.comments )

        #-- 7 --
        result  =  ""
        for i in range(len(outList)):
            result  =  result + outList[i] + "\n"
        result  =  result[:-1]
        return result


# - - -   _ _ c m p _ _   - - -

    def __cmp__ ( self, other ):
        """Compare function for address book entries.  Keys are:
            1. All non-list entries come before all list entries.
            2. Entries with fullnames are sorted by fullname.
            3. Entries without fullnames are sorted by nickname.
        """
        a1  =  ( self.address[0] == "(" )
        a2  =  ( other.address[0] == "(" )
        c   =  cmp ( a1, a2 )
        if  c <> 0:
            return c

        c  =  cmp ( string.upper ( self.fullName ),
                    string.upper ( other.fullName ) )
        if  c <> 0:
            return c

        return cmp ( string.upper ( self.nickname ),
                     string.upper ( other.nickname ) )



# - - -   _ _ a d d F i e l d _ _   - - -

    def __addField ( self, result, s ):
        """Add s to result list with folding on tabs
            [ if (result is empty) ->
                result  +:=  s
              else if ((last line of result + s) would exceed 50 chars) ->
                result  :=  result with ("   " + s appended)
              else ->
                result[-1]  +:=  s
            ]
        """
        if len ( result ) == 0:
            result.append ( s )
        elif ( len ( result[-1] ) + len ( s ) ) > 50:
            result.append ( "   " + s )
        else:
            result[-1]  =  result[-1] + s


# - - -   _ _ b r e a k A d d r e s s   - - -

    def __breakAddress ( self, result, address ):
        """Fold the address portion, breaking between commas.
        """
        brokenList  =  string.split ( address, "," )
        last        =  brokenList[-1]
        del brokenList[-1]

        for b in brokenList:
            self.__addField ( result, b + "," )

        self.__addField ( result, last )



# - - -   U s a g e   - - -

def Usage ( L ):
    """Write a usage message and terminate execution.
        [ if L is a string ->
            sys.stderr  +:=  L
            terminate execution
          if L is a list of strings ->
            sys.stderr  +:=  concatenation of elements of L
            terminate execution
        ]
    """
    if type ( L ) is types.ListType:
        L = string.join ( L, "" )

    if type ( L ) is not types.StringType:
        sys.stderr.write ( "*** Usage routine called with bogus "
                           "argument: " + str(L) + ".\n" )
        raise TypeError, "Bogus argument type to Usage()"
    
    sys.stderr.write ( "*** Usage:\n"
        "  pinemerge f1 f2 ...\n"
        "where f1, f2, ... are Pine addressbooks to be merged.\n" )
    sys.stderr.write ( "*** Error: " + L + "\n" )
    sys.exit ( 1 )


# - - - - -   p i n e m e r g e . p y   - -   m a i n   - - -

#--
# Create an empty address book.
#--

outBook  =  AddrBook ( )

#--
# Read each file named on the command line.  For each file,
# process it into an address book, then attempt to add 
# each entry to outBook.  Duplicates are flagged if any
# of the nickname/fullname/address fields differ.
#--

argList  =  sys.argv[1:]

if  len ( argList ) < 1:
    Usage ( "At least two address book files must be supplied." )

for fileName in argList:
    sys.stderr.write ( "=== Reading %s ===\n" % fileName )
    book  =  AddrBook ( fileName )

    for nickname in book.nickMap.keys():
        addr  =  book.nickMap[nickname]

        try:
            outBook.insert ( addr )
        except:
            other  =  outBook.nickMap[nickname]
            if ( ( addr.fullName <> other.fullName) or
                 ( addr.address <> other.address ) ):
                sys.stderr.write (
                  "\nDuplicate nickname, omitted:\n%s\n"
                  "Conflicts with:\n%s\n" %
                  ( addr.show(), other.show() ) )

#--
# Output the merged address book
#--

outBook.write ( sys.stdout )

TCC home: TCC home
NMT home: NMT home

Last updated: 2009-11-22 09:01 MST