def get_filename(command_basename: str) -> str: ''' Absolute path of the command in the current ``${PATH}`` with the passed basename if found *or* raise an exception otherwise. Parameters ---------- command_basename : str Basename of the command to return the absolute path of. Returns ---------- str Absolute path of this command. Raises ---------- BetseCommandException If no such command is found. ''' # Absolute path of this command if found or "None" otherwise. command_filename = get_filename_or_none(command_basename) # If this command is *NOT* found, raise an exception. if command_filename is None: raise BetseCommandException( 'Command "{}" not found.'.format(command_basename)) # Return this path. return command_filename
def get_first_basename(command_basenames: SequenceTypes, exception_message: str = None) -> str: ''' First pathable string in the passed list (i.e., the first string that is the basename of a command in the current ``${PATH}``) if any *or* raise an exception otherwise. Parameters ---------- command_basenames : SequenceTypes List of the basenames of all commands to be iteratively searched for (in descending order of preference). exception_message : optional[str] Optional exception message to be raised if no such string is pathable. Defaults to ``None``, in which case an exception message synthesized from the passed strings is raised. Returns ---------- str First pathable string in the passed list. Raises ---------- BetseCommandException If no passed strings are pathable. ''' # Avoid circular import dependencies. from betse.util.type.text.string import strjoin # If this list contains the basename of a pathable command, return the # first such basename. for command_basename in command_basenames: if is_pathable(command_basename): return command_basename # Else, this list contains no such basename. Ergo, raise an exception. exception_message_suffix = ( '{} not found in the current ${{PATH}}.'.format( strjoin.join_as_conjunction_double_quoted(*command_basenames))) # If a non-empty exception message is passed, suffix this message with this # detailed explanation. if exception_message: exception_message += ' ' + exception_message_suffix # Else, default this message to this detailed explanation. else: exception_message = exception_message_suffix # Raise this exception. raise BetseCommandException(exception_message)
def die_unless_command(filename: str, reason: StrOrNoneTypes = None) -> None: ''' Raise an exception unless a command with the passed filename exists. Parameters ---------- filename : str Either the basename *or* the absolute or relative filename of the executable file to be validated. reason : optional[str] Human-readable sentence fragment to be embedded in this exception's message (e.g., ``due to "pyside2-tools" not being installed``). Defaults to ``None``, in which case this message has no such reason. Raises ---------- BetseCommandException If this command does *not* exist. See Also ---------- :func:`is_command` Further details. ''' # If this command does *NOT* exist... if not is_command(filename): # Exception message to be raised. message = 'Command "{}" not found'.format(filename) # If an exception reason was passed, embed this reason in this message. if reason is not None: message += ' ({})'.format(reason) # Finalize this message. message += '.' # Raise this exception. raise BetseCommandException(message)
def die_unless_pathable(command_basename: str, exception_message: StrOrNoneTypes = None): ''' Raise an exception with the passed message unless an external command with the passed basename exists (i.e., unless an executable file with this basename resides in the current ``${PATH}``). Raises ---------- BetseCommandException If no external command with this basename exists. ''' # If this pathable is not found, raise an exception. if not is_pathable(command_basename): # If no message was passed, default this message. if exception_message is None: exception_message = ( 'Command "{}" not found in the current ${{PATH}} or ' 'found but not an executable file.'.format(command_basename)) # Raise this exception. raise BetseCommandException(exception_message)
def _init_popen_kwargs( command_words: SequenceTypes, popen_kwargs: MappingOrNoneTypes ) -> MappingType: ''' Sanitized dictionary of all keyword arguments to pass to the :class:`subprocess.Popen` callable when running the command specified by the passed shell words with the passed user-defined keyword arguments. `close_fds` ---------- If the current platform is vanilla Windows *and* none of the ``stdin``, ``stdout``, ``stderr``, or ``close_fds`` arguments are passed, the latter argument will be explicitly set to ``False`` -- causing the command to be run to inherit all file handles (including stdin, stdout, and stderr) from the current process. By default, :class:`subprocess.Popen` documentation insists that: > On Windows, if ``close_fds`` is ``True`` then no handles will be > inherited by the child process. The child process will then open new file handles for stdin, stdout, and stderr. If the current terminal is a Windows Console, the underlying terminal devices and hence file handles will remain the same, in which case this is *not* an issue. If the current terminal is Cygwin-based (e.g.,, MinTTY), however, the underlying terminal devices and hence file handles will differ, in which case this behaviour prevents interaction between the current shell and the vanilla Windows command to be run below. In particular, all output from this command will be squelched. If at least one of stdin, stdout, or stderr are redirected to a blocking pipe, setting ``close_fds`` to ``False`` can induce deadlocks under certain edge-case scenarios. Since all such file handles default to ``None`` and hence are *not* redirected in this case, ``close_fds`` may be safely set to ``False``. On all other platforms, if ``close_fds`` is ``True``, no file handles *except* stdin, stdout, and stderr will be inherited by the child process. This function fundamentally differs in subtle (and only slightly documented ways) between vanilla Windows and all other platforms. These discrepancies appear to be harmful but probably unavoidable, given the philosophical gulf between vanilla Windows and all other platforms. Parameters ---------- command_words : SequenceTypes List of one or more shell words comprising this command. popen_kwargs : optional[MappingType] Dictionary of all keyword arguments to be sanitized if any *or* ``None`` otherwise, in which case the empty dictionary is defaulted to. ''' # Avoid circular import dependencies. from betse.util.path.command import cmds from betse.util.os.brand import windows from betse.util.os.shell import shellenv from betse.util.type.iterable.mapping import maptest # If this list of shell words is empty, raise an exception. if not command_words: raise BetseCommandException('Non-empty command expected.') # If these keyword arguments are empty, default to the empty dictionary. if popen_kwargs is None: popen_kwargs = {} # If the first shell word is this list is unrunnable, raise an exception. cmds.die_unless_command(command_words[0]) # Log the command to be run before doing so. logs.log_debug('Running command: %s', ' '.join(command_words)) # If this is vanilla Windows, sanitize the "close_fds" argument. if windows.is_windows_vanilla() and not maptest.has_keys( mapping=popen_kwargs, keys=('stdin', 'stdout', 'stderr', 'close_fds',)): popen_kwargs['close_fds'] = False # Isolate the current set of environment variables to this command, # preventing concurrent changes in these variables in the current process # from affecting this command's subprocess. popen_kwargs['env'] = shellenv.get_env() # Decode command output with the current locale's preferred encoding. popen_kwargs['universal_newlines'] = True # Return these keyword arguments. return popen_kwargs