BASIS  r3148
utilities.py
Go to the documentation of this file.
00001 ##############################################################################
00002 # @file  utilities.py
00003 # @brief Main module of project-independent BASIS utilities.
00004 #
00005 # This module defines the BASIS Utilities whose implementations are not
00006 # project-specific, i.e., do not make use of particular project attributes such
00007 # as the name or version of the project. The utility functions defined by this
00008 # module are intended for use in Python scripts and modules that are not build
00009 # as part of a particular BASIS project. Otherwise, the project-specific
00010 # implementations should be used instead, i.e., those defined by the basis.py
00011 # module of the project. The basis.py module and the submodules imported by
00012 # it are generated from template modules which are customized for the particular
00013 # project that is being build.
00014 #
00015 # Copyright (c) 2011, 2012 University of Pennsylvania. All rights reserved.<br />
00016 # See https://www.cbica.upenn.edu/sbia/software/license.html or COPYING file.
00017 #
00018 # Contact: SBIA Group <sbia-software at uphs.upenn.edu>
00019 #
00020 # @ingroup BasisPythonUtilities
00021 ##############################################################################
00022 
00023 __all__ = [] # use of import * is discouraged
00024 
00025 import os
00026 import sys
00027 import re
00028 import shlex
00029 import subprocess
00030 
00031 from . import which
00032 
00033 
00034 # ============================================================================
00035 # constants
00036 # ============================================================================
00037 
00038 ## @brief Default copyright of executables.
00039 COPYRIGHT = "2011, 2012, 2013 University of Pennsylvania"
00040 ## @brief Default license of executables.
00041 LICENSE = "See https://www.cbica.upenn.edu/sbia/software/license.html or COPYING file."
00042 ## @brief Default contact to use for help output of executables.
00043 CONTACT = "SBIA Group <sbia-software at uphs.upenn.edu>"
00044 
00045 
00046 # used to make base argument of functions absolute
00047 _MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
00048 
00049 # ============================================================================
00050 # executable information
00051 # ============================================================================
00052 
00053 # ----------------------------------------------------------------------------
00054 ## @brief Print contact information.
00055 #
00056 # @param [in] contact Name of contact.
00057 def print_contact(contact=CONTACT):
00058     sys.stdout.write("Contact:\n  " + contact + "\n")
00059 
00060 # ----------------------------------------------------------------------------
00061 ## @brief Print version information including copyright and license notices.
00062 #
00063 # @param [in] name      Name of executable. Should not be set programmatically
00064 #                       to the first argument of the @c __main__ module, but
00065 #                       a string literal instead.
00066 # @param [in] version   Version of executable, e.g., release of project
00067 #                       this executable belongs to.
00068 # @param [in] project   Name of project this executable belongs to.
00069 #                       If @c None, or an empty string, no project
00070 #                       information is printed.
00071 # @param [in] copyright The copyright notice, excluding the common prefix
00072 #                       "Copyright (c) " and suffix ". All rights reserved.".
00073 #                       If @c None, or an empty string, no copyright notice
00074 #                       is printed.
00075 # @param [in] license   Information regarding licensing. If @c None or an
00076 #                       empty string, no license information is printed.
00077 def print_version(name, version=None, project=None, copyright=COPYRIGHT, license=LICENSE):
00078     if not version: raise Exception('print_version(): Missing version argument')
00079     # program identification
00080     sys.stdout.write(name)
00081     if project:
00082         sys.stdout.write(' (')
00083         sys.stdout.write(project)
00084         sys.stdout.write(')')
00085     sys.stdout.write(' ')
00086     sys.stdout.write(version)
00087     sys.stdout.write('\n')
00088     # copyright notice
00089     if copyright:
00090         sys.stdout.write("Copyright (c) ");
00091         sys.stdout.write(copyright)
00092         sys.stdout.write(". All rights reserved.\n")
00093     # license information
00094     if license:
00095         sys.stdout.write(license)
00096         sys.stdout.write('\n')
00097 
00098 # ----------------------------------------------------------------------------
00099 ## @brief Get UID of build target.
00100 #
00101 # The UID of a build target is its name prepended by a namespace identifier
00102 # which should be unique for each project.
00103 #
00104 # @param [in] name    Name of build target.
00105 # @param [in] prefix  Common prefix of targets belonging to this project.
00106 # @param [in] targets Dictionary mapping target UIDs to executable paths.
00107 #
00108 # @returns UID of named build target.
00109 def targetuid(name, prefix=None, targets=None):
00110     # handle invalid arguments
00111     if not name: return None
00112     # in case of a leading namespace separator, do not modify target name
00113     if name.startswith('.'): return name
00114     # common target UID prefix of project
00115     if prefix is None or not targets: return name
00116     # try prepending namespace or parts of it until target is known
00117     separator = '.'
00118     while True:
00119         if separator.join([prefix, name]) in targets:
00120             return separator.join([prefix, name])
00121         parts = prefix.split(separator, 1)
00122         if len(parts) == 1: break
00123         prefix = parts[0]
00124     # otherwise, return target name unchanged
00125     return name
00126 
00127 # ----------------------------------------------------------------------------
00128 ## @brief Determine whether a given build target is known.
00129 #
00130 # @param [in] name    Name of build target.
00131 # @param [in] prefix  Common prefix of targets belonging to this project.
00132 # @param [in] targets Dictionary mapping target UIDs to executable paths.
00133 #
00134 # @returns Whether the named target is a known executable target.
00135 def istarget(name, prefix=None, targets=None):
00136     uid = targetuid(name, prefix=prefix, targets=targets)
00137     if not uid or not targets: return False
00138     if uid.startswith('.'): uid = uid[1:]
00139     return uid in targets
00140 
00141 # ----------------------------------------------------------------------------
00142 ## @brief Get absolute path of executable file.
00143 #
00144 # This function determines the absolute file path of an executable. If no
00145 # arguments are given, the absolute path of this executable is returned.
00146 # If the command names a known executable build target, the absolute path to
00147 # the corresonding built (and installed) executable file is returned.
00148 # Otherwise, the named command is searched in the system @c PATH and its
00149 # absolute path returned if found. If the executable is not found, @c None
00150 # is returned.
00151 #
00152 # @param [in] name    Name of command or @c None.
00153 # @param [in] prefix  Common prefix of targets belonging to this project.
00154 # @param [in] targets Dictionary mapping target UIDs to executable paths.
00155 # @param [in] base    Base directory for relative paths in @p targets.
00156 #
00157 # @returns Absolute path of executable or @c None if not found.
00158 #          If @p name is @c None, the path of this executable is returned.
00159 def exepath(name=None, prefix=None, targets=None, base='.'):
00160     path = None
00161     if name is None:
00162         path = os.path.realpath(sys.argv[0])
00163     elif istarget(name, prefix=prefix, targets=targets):
00164         uid = targetuid(name, prefix=prefix, targets=targets)
00165         if uid.startswith('.'): uid = uid[1:]
00166         path = os.path.normpath(os.path.join(os.path.join(_MODULE_DIR, base), targets[uid]))
00167         if '$(IntDir)' in path:
00168             for intdir in ['Release', 'Debug', 'RelWithDebInfo', 'MinSizeRel']:
00169                 tmppath = path.replace('$(IntDir)', intdir)
00170                 if os.path.isfile(tmppath):
00171                     path = tmppath
00172                     break
00173             path = path.replace('$(IntDir)', '')
00174     else:
00175         try:
00176             path = which.which(name)
00177         except which.WhichError:
00178             pass
00179     return path
00180 
00181 # ----------------------------------------------------------------------------
00182 ## @brief Get name of executable file.
00183 #
00184 # @param [in] name    Name of command or @c None.
00185 # @param [in] prefix  Common prefix of targets belonging to this project.
00186 # @param [in] targets Dictionary mapping target UIDs to executable paths.
00187 # @param [in] base    Base directory for relative paths in @p targets.
00188 #
00189 # @returns Name of executable file or @c None if not found.
00190 #          If @p name is @c None, the name of this executable is returned.
00191 def exename(name=None, prefix=None, targets=None, base='.'):
00192     path = exepath(name, prefix, targets, base)
00193     if path is None: return None
00194     name = os.path.basename(path)
00195     if os.name == 'nt' and (name.endswith('.exe') or name.endswith('.com')):
00196         name = name[:-4]
00197     return name
00198 
00199 # ----------------------------------------------------------------------------
00200 ## @brief Get directory of executable file.
00201 #
00202 # @param [in] name    Name of command or @c None.
00203 # @param [in] prefix  Common prefix of targets belonging to this project.
00204 # @param [in] targets Dictionary mapping target UIDs to executable paths.
00205 # @param [in] base    Base directory for relative paths in @p targets.
00206 #
00207 # @returns Absolute path of directory containing executable or @c None if not found.
00208 #         If @p name is @c None, the directory of this executable is returned.
00209 def exedir(name=None, prefix=None, targets=None, base='.'):
00210     path = exepath(name, prefix, targets, base)
00211     if path is None: return None
00212     return os.path.dirname(path)
00213 
00214 # ============================================================================
00215 # command execution
00216 # ============================================================================
00217 
00218 # ----------------------------------------------------------------------------
00219 ## @brief Exception thrown when command execution failed.
00220 class SubprocessError(Exception):
00221     ## @brief Initialize exception, i.e., set message describing failure.
00222     def __init__(self, msg):
00223         self._message = msg
00224     ## @brief Return string representation of exception message.
00225     def __str__(self):
00226         return self._message
00227 
00228 # ----------------------------------------------------------------------------
00229 ## @brief Convert array of arguments to quoted string.
00230 #
00231 # @param [in] args Array of arguments.
00232 #
00233 # @returns Double quoted string, i.e., string where arguments are separated
00234 #          by a space character and surrounded by double quotes if necessary.
00235 #          Double quotes within an argument are escaped with a backslash.
00236 #
00237 # @sa split_quoted_string()
00238 def tostring(args):
00239     qargs = []
00240     re_quote_or_not = re.compile(r"'|\s|^$")
00241     for arg in args:
00242         # escape double quotes
00243         arg = arg.replace('"', '\\"')
00244         # surround element by double quotes if necessary
00245         if re_quote_or_not.search(arg): qargs.append(''.join(['"', arg, '"']))
00246         else:                           qargs.append(arg)
00247     return ' '.join(qargs)
00248 
00249 # ----------------------------------------------------------------------------
00250 ## @brief Split quoted string of arguments.
00251 #
00252 # @param [in] args Quoted string of arguments.
00253 #
00254 # @returns Array of arguments.
00255 #
00256 # @sa to_quoted_string()
00257 def qsplit(args):
00258     return shlex.split(args)
00259 
00260 # ----------------------------------------------------------------------------
00261 ## @brief Execute command as subprocess.
00262 #
00263 # @param [in] args       Command with arguments given either as quoted string
00264 #                        or array of command name and arguments. In the latter
00265 #                        case, the array elements are converted to strings
00266 #                        using the built-in str() function. Hence, any type
00267 #                        which can be converted to a string is permitted.
00268 #                        The first argument must be the name or path of the
00269 #                        executable of the command.
00270 # @param [in] quiet      Turns off output of @c stdout of child process to
00271 #                        stdout of parent process.
00272 # @param [in] stdout     Whether to return the command output.
00273 # @param [in] allow_fail If true, does not raise an exception if return
00274 #                        value is non-zero. Otherwise, a @c SubprocessError is
00275 #                        raised by this function.
00276 # @param [in] verbose    Verbosity of output messages.
00277 #                        Does not affect verbosity of executed command.
00278 # @param [in] simulate   Whether to simulate command execution only.
00279 # @param [in] prefix     Common prefix of targets belonging to this project.
00280 # @param [in] targets    Dictionary mapping target UIDs to executable paths.
00281 # @param [in] base       Base directory for relative paths in @p targets.
00282 #
00283 # @return The exit code of the subprocess if @p stdout is false (the default).
00284 #         Otherwise, if @p stdout is true, a tuple consisting of exit code
00285 #         command output is returned. Note that if @p allow_fail is false,
00286 #         the returned exit code will always be 0.
00287 #
00288 # @throws SubprocessError If command execution failed. This exception is not
00289 #                         raised if the command executed with non-zero exit
00290 #                         code but @p allow_fail set to @c True.
00291 def execute(args, quiet=False, stdout=False, allow_fail=False, verbose=0, simulate=False,
00292                   prefix=None, targets=None, base='.'):
00293     # convert args to list of strings
00294     if   type(args) is list:            args = [str(i) for i in args]
00295     elif type(args) in (str, unicode):  args = qsplit(args);
00296     else:              raise Exception("execute(): Argument args must be either list or string, but %s given" % type(args))
00297     if len(args) == 0: raise Exception("execute(): No command specified for execution")
00298     # get absolute path of executable
00299     path = exepath(args[0], prefix=prefix, targets=targets, base=base)
00300     if not path: raise SubprocessError(args[0] + ": Command not found")
00301     args[0] = path
00302     # some verbose output
00303     if verbose > 0 or simulate:
00304         sys.stdout.write('$ ')
00305         sys.stdout.write(tostring(args))
00306         if simulate: sys.stdout.write(' (simulated)')
00307         sys.stdout.write('\n')
00308     # execute command
00309     status = 0
00310     output = ''
00311     if not simulate:
00312         try:
00313             # open subprocess
00314             process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
00315             # read stdout until EOF
00316             for line in process.stdout:
00317                 if stdout:
00318                     output = ''.join([output, line])
00319                 if not quiet:
00320                     print line.rstrip()
00321                     sys.stdout.flush()
00322             # wait until subprocess terminated and set exit code
00323             (out, err) = process.communicate()
00324             # print error messages of subprocess
00325             for line in err: sys.stderr.write(line);
00326             # get exit code
00327             status = process.returncode
00328         except OSError, e:
00329             raise SubprocessError(args[0] + ': ' + str(e))
00330         except Exception, e:
00331             msg  = "Exception while executing \"" + args[0] + "\"!\n"
00332             msg += "\tArguments: " + tostring(args[1:]) + '\n'
00333             msg += '\t' + str(e)
00334             raise SubprocessError(msg)
00335     # if command failed, throw an exception
00336     if status != 0 and not allow_fail:
00337         raise SubprocessError("** Failed: " + tostring(args))
00338     # return
00339     if stdout: return (status, output)
00340     else:      return status
00341 
00342 
00343 ## @}
00344 # end of Doxygen group