Too Many Computers

Between work and home, including real hardware and VM instances, I find myself hopping backwards and forwards between 5 or 6 different machines at any given time. I also happen to use the PuTTY SSH client quite a lot. I’ve got a bunch of PuTTY shortcuts configured across my little ‘herd’ of computers to give me quick access to the systems that I access most frequently.The problem is, the shortcuts aren’t exactly the same on all of my machines.

I use Windows-based computers with both 32 and 64 bit versions of the OS installed, which means that the PuTTY binary will be in a different location between, for example, a 32-bit Windows XP computer, a 32-bit Windows7 computer and a 64-bit Windows7 computer. This makes synchronisation of my folder of SSH shortcuts across all machines slightly problematical – if I change a shortcut on the 32-bit XP machine, I can’t just copy the changed .lnk file across to one of my 64-bit Windows7 boxes and expect it to work. With this in mind, I wrote a short Python script to deal with this for me. It will recursively scan a folder containing .lnk files and update each one so that it points off to the right place to launch a PuTTY session.

Note that this script depends upon you having Mark Hammonds ‘Python for Windows’ tool kit installed. If you haven’t got this, you can download it from SourceForge. For my script, either copy and paste the code below or scroll down for a direct download link.

This version (v 1.2) incorporates a bug fix which ensures that it only updates shortcuts which refer to ‘putty.exe’, otherwise it can end up overwriting your entire Start menu if you give it the right path.

#!/usr/bin/python
# Check and adjust PuTTY SSH shortcuts
# vim: ai et sw=4 ts=4
#
# Copyright (C)2012 Phil Edwards <phil 'at' linux2000.com>
#
# License: 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 3 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.
#
# CHANGELOG:
#
# Ver    Date       Who  Changes
# -----  ---------  ---  ---------------------------------------------------
# 1.0    21SEP2012  PKE  First version
# 1.1    06MAR2013  PKE  Enhanced command line option processing
# 1.2    16MAY2013  PKE  Bug fix - only update shortcuts that contain
#                        putty.exe in the target path

import os
import sys
import time
from optparse import OptionParser
import pythoncom
from win32com.shell import shell

class Win32Shortcut:
    def __init__(self, lnkname):
        self.shortcut = pythoncom.CoCreateInstance(
            shell.CLSID_ShellLink, None,
            pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
        self.shortcut.QueryInterface(pythoncom.IID_IPersistFile).Load(lnkname)

    def __getattr__(self, name):
        return getattr(self.shortcut, name)

class RunTimeOptions:
    def __init__(self):
        self.__me__ = os.path.basename(sys.argv[0])
        self.__version__ = '1.2'
        self.__copyright__ = "Copyright (C)2013 Phil Edwards <phil 'at' linux2000.com> All Rights Reserved"
        self.__summary__ = 'Automatic PuTTY shortcut fixer'
       
        self.puttyPath = None
        self.found = 0
        self.updated = 0
        self.defaultPath = os.environ['appdata'] + "\Microsoft\Internet Explorer\Quick Launch"
       
        if os.path.exists("C:\Program Files (x86)\PuTTY\putty.exe"):
            self.puttyPath = 'C:\Program Files (x86)\PuTTY\putty.exe'

        if os.path.exists("C:\Program Files\PuTTY\putty.exe"):
            self.puttyPath = 'C:\Program Files\PuTTY\putty.exe'
   
        self.getCommandLineOptions()

        self.logMsg(txt = '%s v%s %s' % (self.__me__, self.__version__, self.__summary__))
        self.logMsg(txt = 'command line arguments: [%s]' % ' '.join(sys.argv[1:]))
        self.logMsg(txt = 'Executable found at [%s]' % self.puttyPath)

    def logMsg(self, **kwargs):
        txt = kwargs.get('txt', '')
        dateStamp = kwargs.get('dateStamp', True)
        msgLine = ''

        if txt != '':
            if dateStamp: msgLine += '[' + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + '] '
            if not self.doUpdate: msgLine += 'NOUPDATE: '
            msgLine += txt
            print msgLine

    def debugMsg(self, **kwargs):
        txt = kwargs.get('txt', '')
        dateStamp = kwargs.get('dateStamp', True)
        pause = kwargs.get('pause', False)
        msgLine = ''

        if self.debugMode and txt != '':
            if dateStamp: msgLine += '[' + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + '] '
            msgLine += 'DEBUG: ' + txt
            if pause:
                bull = raw_input(msgLine + ': Press any key to continue : ')
            else:
                print msgLine

    def getCommandLineOptions(self):
        vString = '%%prog (%s) v%s' % (self.__summary__, self.__version__)

        dString = 'Script to examine PuTTY SSH shortcuts found in the folder '
        dString += 'referenced by BASEPATH and update them so that '
        dString += 'they contain the correct path to the PuTTY executable'

        eString = ''
       
        uString = 'usage: %prog [options] BASEPATH'

        parser = OptionParser(version = vString, description = dString,
            epilog = eString, usage = uString)

        parser.add_option('-d', '--dry-run',
            action = 'store_false', dest = 'doupdate', default = True,
            help = 'process as normal, but do not update any files')

        parser.add_option('-x', '--debug',
            action = 'store_true', dest = 'debugmode', default = False,
            help = 'emit debug messages for troubleshooting')

        parser.add_option('-v', '--verbose',
            action = 'store_true', dest = 'verbose', default = False,
            help = 'be more verbose when processing')

        (options, args) = parser.parse_args()
       
        if len(args) == 0:
            self.basePath = self.defaultPath
        elif len(args) == 1:
            self.basePath = args[0]
        else:
            print 'ERROR: ambiguous command line parameters'
            sys.exit(0)

        if self.puttyPath is None:
            print 'ERROR: Unable to locate PuTTY executable'
            sys.exit(0)
           
        if not os.path.exists(self.basePath):
            print 'ERROR: base path does not exist'
            sys.exit(0)

        self.doUpdate = options.doupdate
        self.debugMode = options.debugmode
        self.verbose = options.verbose

        self.debugMsg(txt = 'options = %s' % options)
        self.debugMsg(txt = 'args = %s' % args)

def processItem(my, path):
    my.found += 1
   
    s = Win32Shortcut(path)
    iconPath = s.GetIconLocation()[0]
    itemPath = s.GetPath(0)[0]
    my.debugMsg(txt = 'itemPath is %s' % itemPath, pause = True)

    if itemPath != my.puttyPath and 'putty.exe' in itemPath.lower():
        s.SetPath(my.puttyPath)
        pf = s.QueryInterface (pythoncom.IID_IPersistFile)
        if my.doUpdate: pf.Save(path, 0)
        my.updated += 1
        my.logMsg(txt = '%s updated' % path)
    else:
        if my.verbose:
            my.logMsg(txt = '%s OK' % path)

def processFolder(my, path):
    if not os.path.isdir(path): return
   
    if my.verbose:
        my.logMsg(txt = 'Processing folder [%s]' % path)
       
    flist = os.listdir(path)
    for item in flist:
        newPath = os.path.join(path, item)
        if os.path.isdir(newPath):
            processFolder(my, newPath)
        elif item.endswith('.lnk'):
            processItem(my, os.path.join(path, item))

def main():
    my = RunTimeOptions()

    my.logMsg(txt = 'Processing started')
    processFolder(my, my.basePath)
    my.logMsg(txt = '%s shortcuts found' % my.found)
    my.logMsg(txt = '%s shortcuts updated' % my.updated)
    my.logMsg(txt = 'Processing complete')

if __name__ == '__main__':
    main()
Download1312 downloads

Python Command Line Script Template

Despite having used Python as my scripting language of choice for several years, I still find myself stumbling across the odd ‘hidden gem’ when searching through the ‘global module index’, which is part of the documentation that comes with the language.

A fine example of this cropped up recently when I was writing some command line utilities to help with a particular project. I like to make these things as user-friendly as possible, mainly so that if someone else has to use them, they can do so without me having to write reams of documentation explaining how each one works. I also have an intense dislike for the process of re-inventing wheels, it therefore made sense to come up with some boilerplate code that I could use for each script which covered just the basics of getting command line options, providing a help feature and allowing for debug messages to be added for diagnostic purposes when testing the code.

Python comes with several modules which deal with command line option processing, each of which has its own strengths and weaknesses. The code template shown here uses the optparse module, which is able to provide all of the features that I wanted to have in my scripts, without making me re-code everything by hand for each one. I wanted each script to:

  • Provide online help for available command line options, using the accepted convention of running the script with a ‘-h’ or ‘–help’ argument.
  • Support a ‘-v’ or ‘–verbose’ argument so that there is the option to provide more detailed output when the script is run.
  • Support the use of a ‘–dry-run’ option so that a script can show what it would do, but not actually make any changes to input data or files
  • Have predefined functions for displaying status and/or debug messages

The resulting code template runs to a rather svelte 120 lines. Of these, 19 lines are comments and 24 are blank lines inserted for readability. The code below also has some extra examples of how you might, for example, specify an input file to be processed, or restrict processing to the first few lines of input data only.

You can either copy and paste the code below, or scroll down for a download link:

#!/usr/bin/python
# Skeleton script with command line parsing
# vim: ai et sw=4 ts=4
#
# Copyright (C)2012 Phil Edwards <phil 'at' linux2000.com>
#
# License: 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 3 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.
#
# CHANGELOG:
#
# Ver    Date       Who  Changes
# -----  ---------  ---  ---------------------------------------------------
# 1.0    21SEP2012  PKE  First version

import os
import sys
import time
from optparse import OptionParser

class RunTimeOptions:
    def __init__(self):
        self.__me__ = os.path.basename(sys.argv[0])
        self.__version__ = '1.0'
        self.__copyright__ = "Copyright (C)2012 Phil Edwards <phil 'at' linux2000.com> All Rights Reserved"
        self.__summary__ = 'Awesome Python script written by Phil'
        self.getCommandLineOptions()
        self.validateOptions()

        self.logMsg(txt = '%s v%s %s' % (self.__me__, self.__version__, self.__summary__))
        self.logMsg(txt = 'command line arguments: [%s]' % ' '.join(sys.argv[1:]))

    def logMsg(self, **kwargs):
        txt = kwargs.get('txt', '')
        dateStamp = kwargs.get('dateStamp', True)
        msgLine = ''

        if txt != '':
            if dateStamp: msgLine += '[' + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + '] '
            if not self.doUpdate: msgLine += 'NOUPDATE: '
            msgLine += txt
            print msgLine

    def debugMsg(self, **kwargs):
        txt = kwargs.get('txt', '')
        dateStamp = kwargs.get('dateStamp', True)
        pause = kwargs.get('pause', False)
        msgLine = ''

        if self.debugMode and txt != '':
            if dateStamp: msgLine += '[' + time.strftime("%Y%m%d-%H%M%S", time.localtime()) + '] '
            msgLine += 'DEBUG: ' + txt
            if pause:
                bull = raw_input(msgLine + ': Press any key to continue : ')
            else:
                print msgLine

    def getCommandLineOptions(self):
        vString = '%%prog (%s) v%s' % (self.__summary__, self.__version__)

        dString = 'A super awesome Python script. Phil wrote it, so it must be good! '
        dString += 'It does all sorts of really cool stuff, and even has some flashing '
        dString += 'lights to keep you amused.'

        eString = 'OK, maybe I lied about the flashing lights, but the rest '
        eString += 'of it is really good'

        parser = OptionParser(version = vString, description = dString, epilog = eString)

        parser.add_option('-d', '--dry-run',
            action = 'store_false', dest = 'doupdate', default = True,
            help = 'process as normal, but do not update any files')

        parser.add_option('-x', '--debug',
            action = 'store_true', dest = 'debugmode', default = False,
            help = 'emit debug messages for troubleshooting')

        parser.add_option('-i', '--input', dest = 'inputfile',
            #action = 'append', # lets you have more than one file if required
            help = 'read input data from FILE, which must exist', metavar = 'FILE')

        parser.add_option('-l', '--limit', dest = 'limit', type='int',
            help = 'limit processing to the first NUM items found', metavar = 'NUM')

        parser.add_option('-v', '--verbose',
            action = 'store_true', dest = 'verbose', default = False,
            help = 'be more verbose when processing')

        (options, args) = parser.parse_args()

        self.doUpdate = options.doupdate
        self.debugMode = options.debugmode
        self.verbose = options.verbose
        self.inputfile = options.inputfile

        self.debugMsg(txt = 'options = %s' % options)
        self.debugMsg(txt = 'args = %s' % args)

    def validateOptions(self):
        if self.inputfile is not None and not os.path.exists(self.inputfile):
            self.logMsg(txt = 'ERROR: file %s does not exist' % self.inputfile, dateStamp = False)
            sys.exit(0)

def main():
    my = RunTimeOptions()

    my.logMsg(txt = 'Processing started')
    my.debugMsg(txt = 'This is a debug message with no datestamp', dateStamp=False)
    my.debugMsg(txt = 'This debug message has a datestamp')
    my.debugMsg(txt = 'Debug message with datestamp and a pause', pause=True)
    my.debugMsg(txt = 'Debug message without datestamp, with a pause', dateStamp=False, pause=True)

    my.logMsg(txt = 'Processing complete')

if __name__ == '__main__':
    main()
Download a copy1452 downloads