Next / Previous / Contents / TCC Help System / NM Tech homepage

9. Source code for softlinks.py

The overall flow of this script is similar to that for bigfiles.py. If any directory paths are given on the command line, a report is printed for each one. If there are no command line arguments, one report is printed for the current directory “.”.

As the os.path.walk() function visits every directory and file in a tree, the visitor function finds the soft links, and accumulates a list of PathInfo instances for each one. Those instances are sorted by path name (which is the default ordering for PathInfo instances), then the entries in the sorted list are displayed in the body of the report.

9.1. softlinks.py: Code prologue

The script starts with the usual “pound bang line”, a comment pointing to this documentation, and constants defining the script name and external version number.

softlinks.py
#!/usr/bin/env python
#================================================================
# softlinks.py:  Script to find all soft links a directory tree.
#   For documentation in "literate programming" style, see:
#     http://www.nmt.edu/tcc/help/lang/python/examples/pathinfo/
#----------------------------------------------------------------

SCRIPT_NAME  =  "softlinks.py"
EXTERNAL_VERSION  =  "1.0"

Next we import the usual modules for the system and operating system interfaces, and of course the PathInfo class.

softlinks.py
#================================================================
# Imports
#----------------------------------------------------------------
import sys, os
import pathinfo

9.2. main(): Main program

The main starts by building the list of directories from the command line, defaulting to [ "." ] if there are no command line arguments.

softlinks.py
# - - - - -   m a i n   - - - - -

def main():
    """Main program."""
    print "=== %s %s ===" % (SCRIPT_NAME, EXTERNAL_VERSION)

    #-- 1 --
    # [ if sys.argv[1:] is empty ->
    #     dirList  :=  [ "." ]
    #   else ->
    #     dirList  :=  sys.argv[1:] ]
    dirList  =  sys.argv[1:]
    if  len(dirList) == 0:
        dirList  =  [ "." ]

Next we generate a report for each member of dirList.

softlinks.py
    #-- 2 --
    # [ sys.stdout  +:=  reports listing links below each
    #       directory named in dirList, with files in
    #       ascending order by pathname ]
    for  dirName in dirList:
        #-- 2 body --
        # [ dirName is a string ->
        #     sys.stdout  +:=  a report listing the files below
        #         directory (dirName), with files in ascending
        #         order by pathname ]
        report ( dirName )

9.3. report(): Generate one tree's report

This function generates the portion of the output for one directory subtree.

softlinks.py
# - - -   r e p o r t   - - -

def report ( dirName ):
    """Generate the report for one directory subtree.

      [ dirName is a string ->
          sys.stdout  +:=  a report listing the files below
              directory (dirName), with files in ascending
              order by pathname ]
    """

At this level, the logic is broken into three steps: write the report header; instantiate a LinkReport object containing information on all the links in the subtree; and call the LinkReport.genLinks() method to generate the lines of the report.

softlinks.py
    #-- 1 --
    # [ basePath  :=  dirName's absolute path name
    #   sys.stdout  +:=  report heading showing dirName's real
    #                    absolute path ]
    basePath  =  os.path.abspath ( dirName )
    print "\n   === %s ===" % os.path.realpath ( dirName )

The basePath is the path to the root directory of the subtree, so it will be a prefix to every pathname under it. The report will remove this prefix, showing each link's path name relative to basePath. For details of the report format, see Section 5, “softlinks.py: Find soft links in a directory tree”.

softlinks.py
    #-- 2 --
    # [ linkReport  :=  a LinkReport instance describing all the
    #       accessible soft links in directory tree (basePath) ]
    linkReport  =  LinkReport ( basePath )

A LinkReport instance is basically a container for LinkInfo instances. The LinkReport.genLinks() method generates these LinkInfo instances in sorted order. Formatting the report is handled by the .__str__() special method in the LinkInfo class.

softlinks.py
    #-- 3 --
    # [ linkReport is a LinkReport instance ->
    #     sys.stdout  +:=  lines describing links in linkReport
    #                      in ascending order by path name ]
    for  linkInfo in linkReport.genLinks():
        print linkInfo

9.4. class LinkReport: Link report container

An instance of LinkReport is a container for information about the links in a given directory subtree. The information about each link is represented as a LinkInfo instance; see Section 9.8, “class LinkInfo: The PathInfo subclass”.

softlinks.py
# - - - - -   c l a s s   L i n k R e p o r t   - - - - -

class LinkReport:
    """Holds the links report for one directory subtree.

      Exports:
        LinkReport ( dir ):
          [ dir is a string ->
              if dir names a directory to which we have access ->
                return a LinkReport object describing all the 
                accessible links in that directory's subtree
              else -> raise OSError ]
        .genLinks():
          [ generate the links in self as a sequence of
            LinkInfo instances, in ascending order by path name ]

      Class invariants:
        .__linkList:
          [ a list of LinkInfo objects representing the soft
            links in self ]            
    """

9.5. LinkReport.__init__(): Constructor

The constructor starts by initializing self.__linkList as an empty list.

softlinks.py
# - - -   L i n k R e p o r t . _ _ i n i t _ _   - - -

    def __init__ ( self, dir ):
        """Constructor for the LinkReport class."""

        #-- 1 --
        self.__linkList  =  []

The os.path.walk function calls our .__visitor() method once for each directory in and under dir, passing it a list of the names in that directory; see Section 9.6, “LinkReport.__visitor(): Visitor function for os.path.walk(). The end result is to add one LinkInfo instance to self.__linkList for each soft link in the tree.

softlinks.py
        #-- 2 --
        # [ dir is a string ->
        #     self.__linkList  :=  self.__linkList with LinkInfo
        #         instances added representing every accessible
        #         soft link in the subtree rooted in dir ]
        os.path.walk ( dir, self.__visitor, dir )

All that remains is to sort the links by path name.

softlinks.py
        #-- 3 --
        # [ self.__linkList  :=  self.__linkList, sorted ]
        self.__linkList.sort()

9.6. LinkReport.__visitor(): Visitor function for os.path.walk()

This function is called once for each directory in the subtree. The first argument is the base pathname for the subtree, so that we can generate path names relative to there. The second argument is the directory we are visiting. The third argument is a list of all the names in the directory.

softlinks.py
# - - -   L i n k R e p o r t . _ _ v i s i t o r   - - -

    def __visitor ( self, basePath, dirName, nameList ):
        """Visitor function for os.path.walk.

          [ (basePath is the absolute path to a directory
            at or above dirName) and
            (dirName is the name of a directory) and
            (nameList is a list of the names in that directory) ->
              self.__linkList  :=  self.__linkList with LinkInfo
                  instances added representing accessible links
                  in nameList ]
        """

The names in nameList are the files and soft links in this directory. We iterate through that list, adding more LinkInfo instances to self.__linkList corresponding to accessible soft links. The full path to each file is reconstructed by using the os.path.join() function to prepend dirName.

softlinks.py
        #-- 1 --
        for  fileName in nameList:
            #-- 1 body --
            # [ if fileName names an accessible soft link ->
            #     self.__linkList  :=  self.__linkList + (a new
            #         LinkInfo instance describing fileName)
            #   else -> I ]

            #-- 1.1 --
            # [ filePath  :=  dirName + fileName ]
            filePath  =  os.path.join ( dirName, fileName )

            #-- 1.2 --
            # [ if filePath is an accessible soft link ->
            #     self.__linkList  :=  self.__linkList + (a new
            #         LinkInfo instance describing filePath) ]
            try:
                linkInfo  =  LinkInfo ( filePath, basePath )
                if  linkInfo.isLink():
                    self.__linkList.append ( linkInfo )
            except OSError, detail:
                pass

9.7. LinkReport.genLinks(): Generate links

softlinks.py
# - - -   L i n k R e p o r t . g e n L i n k s   - - -

    def genLinks ( self ):
        """Generate the LinkInfo instances in self."""
        for  linkInfo in self.__linkList:
            yield linkInfo

        raise StopIteration

9.8. class LinkInfo: The PathInfo subclass

This class is derived from PathInfo. It takes an additional constructor argument: the base path name for the directory subtree, necessary so we can display the path name for this link relative to that base path name.

softlinks.py
# - - - - -   c l a s s   L i n k I n f o   - - - - -

class LinkInfo(pathinfo.PathInfo):
    """Represents information about one file; sorts by path name.

      Exports (other than those inherited):
        LinkInfo ( path, basePath ):
          [ (path is the path name to a file) and
            (basePath is the path name of a directory above path) ->
              return a new LinkInfo instance with those values ]
        .__str__ ( self ):
          [ return a multiline string describing self's path,
            whether or not it is a broken link, and where it
            points ]

      State/Invariants:
        .__basePath:     [ as passed to constructor ]
    """

9.9. LinkInfo.__init__(): Constructor

This constructor first calls the parent class's constructor, then stores away the basePath argument within the instance.

softlinks.py
# - - -   L i n k I n f o . _ _ i n i t _ _   - - -

    def __init__ ( self, path, basePath ):
        """Constructor for LinkInfo."""

        #-- 1 --
        pathinfo.PathInfo.__init__ ( self, path )

        #-- 2 --
        self.__basePath  =  basePath

9.10. LinkInfo.__str__(): Convert to a string

This method returns a string containing two lines of output. The format is described in Section 5, “softlinks.py: Find soft links in a directory tree”. The class variables goodPrefix and badPrefix should be the same length; the former is used for links to existing files, the latter for broken links.

softlinks.py
# - - -   L i n k I n f o . _ _ s t r _ _   - - -

    goodPrefix    =  "        "
    brokenPrefix  =  "   #### "

    def __str__ ( self ):
        """Convert a LinkInfo instance to a string.
        """

First we convert the link's absolute path name to a relative path name by removing self.__basePath from the front of it. This relies on the precondition that self.__basePath is a directory above self.path.

softlinks.py
        #-- 1 --
        # [ fullPath  := self's absolute path without links
        #                replaced
        #   targetPath  :=  self's target path ]
        fullPath  =  self.absPath()
        targetPath  =  self.realPath()

        #-- 2 --
        # [ self.__basePath is a directory above fullPath ->
        #     relPath  :=  path to fullPath relative to
        #                  self.__basePath ]
        relPath  =  fullPath [ len(self.__basePath) + 1 : ]

Next we use os.path.exists() to find out whether the link is broken or not, and set prefix to indicate whether it is broken. Finally we format the two-line report string and return it.

softlinks.py
        #-- 2 --
        # [ if  targetPath exists ->
        #     prefix  :=  self.goodPrefix
        #   else ->
        #     prefix  :=  self.brokenPrefix ]
        if  os.path.exists ( targetPath ):
            prefix  =  self.goodPrefix
        else:
            prefix  =  self.brokenPrefix

        #-- 3 --
        return ( "%s ->\n%s%s" %
                 (relPath, prefix, targetPath) )

9.11. Epilogue

softlinks.py
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

if  __name__  ==  "__main__":
    main()