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