如何在Python中判断Windows上的文件是否可执行?

7

我正在编写一个名为 grepath 的实用工具,可以在 %PATH% 中查找与模式匹配的可执行文件。

我需要确定路径中给定的文件名是否可执行(重点在于命令行脚本)。

基于“Tell if a file is executable”,我得到了以下代码:

import os
from pywintypes import error
from win32api   import FindExecutable, GetLongPathName

def is_executable_win(path):
    try:
        _, executable = FindExecutable(path)
        ext = lambda p: os.path.splitext(p)[1].lower()
        if (ext(path) == ext(executable) # reject *.cmd~, *.bat~ cases
            and samefile(GetLongPathName(executable), path)):
            return True
        # path is a document with assoc. check whether it has extension
        # from %PATHEXT% 
        pathexts = os.environ.get('PATHEXT', '').split(os.pathsep)
        return any(ext(path) == e.lower() for e in pathexts)
    except error:
        return None # not an exe or a document with assoc.

samefile指的是:

try: samefile = os.path.samefile
except AttributeError:    
    def samefile(path1, path2):
        rp = lambda p: os.path.realpath(os.path.normcase(p))
        return rp(path1) == rp(path2)

在给定的上下文中,如何改进is_executable_win?Win32 API中的哪些函数可以帮助?

P.S.

  • 时间性能不重要
  • subst驱动器和UNC、Unicode路径不在考虑范围内
  • 如果使用 Windows XP 上可用的函数,则 C++ 答案可以接受

示例

  • notepad.exe is executable (as a rule)
  • which.py is executable if it is associated with some executable (e.g., python.exe) and .PY is in %PATHEXT% i.e., 'C:\> which' could start:

    some\path\python.exe another\path\in\PATH\which.py
    
  • somefile.doc most probably is not executable (when it is associated with Word for example)

  • another_file.txt is not executable (as a rule)
  • ack.pl is executable if it is associated with some executable (most probably perl.exe) and .PL is in %PATHEXT% (i.e. I can run ack without specifing extension if it is in the path)
在这个问题中,“可执行文件”是什么意思?
def is_executable_win_destructive(path):
    #NOTE: it assumes `path` <-> `barename` for the sake of example
    barename = os.path.splitext(os.path.basename(path))[0]
    p = Popen(barename, stdout=PIPE, stderr=PIPE, shell=True)
    stdout, stderr = p.communicate()
    return p.poll() != 1 or stdout != '' or stderr != error_message(barename)

error_message()的内容与语言相关。英文版本如下:

def error_message(barename):
    return "'%(barename)s' is not recognized as an internal" \
           " or external\r\ncommand, operable program or batch file.\r\n" \
           %  dict(barename=barename)

如果is_executable_win_destructive()返回值为真,则表示该路径指向可执行文件。以下是示例:
>>> path = r"c:\docs\somefile.doc"
>>> barename = "somefile"

接下来程序会执行 %COMSPEC%(默认是 cmd.exe):

c:\cwd> cmd.exe /c somefile

如果输出如下所示:
'somefile' 不是内部或外部命令,也不是可运行的程序或批处理文件。
那么 path 不是可执行文件,否则它就是(为了举例说明,让我们假设在pathbarename之间存在一对一的对应关系)。
另一个例子:
>>> path = r'c:\bin\grepath.py'
>>> barename = 'grepath'

如果.PY%PATHEXT%中,并且c:\bin%PATH%中,则执行以下操作:
c:\docs> grepath
Usage:
  grepath.py [options] PATTERN
  grepath.py [options] -e PATTERN

grepath.py: error: incorrect number of arguments

上述输出与error_message(barename)不相等,因此'c:\bin\grepath.py'是一个“可执行文件”。
那么问题是如何在不实际运行它的情况下找出path是否会产生错误?Win32 API函数和用于触发“is not recognized as an internal..”错误的条件是什么?

就操作系统而言,我认为在处理.py和.txt文件时没有区别。它们都是由某个可执行文件打开的。 - shoosh
@shoosh:我在我的问题中添加了一个名为“这个问题中的'可执行'是什么”的部分。它解释了在我的情况下.py和.txt之间的区别是什么。 - jfs
6个回答

3

shoosh比我更快一步 :)

如果我没记错的话,你应该尝试读取文件中的前2个字符。如果返回的是"MZ",那么这就是一个exe文件。


hnd = open(file,"rb")
if hnd.read(2) == "MZ":
  print "exe"

我已经添加了我认为可执行的示例。我不仅仅对exe文件感兴趣。 - jfs

3
我认为,以下内容应该足够:
  1. 检查PATHEXT中的文件扩展名——是否可直接执行
  2. 使用cmd.exe命令"assoc .ext",您可以看到文件是否与某个可执行文件相关联(当您启动此文件时,将启动某个可执行文件)。您可以解析不带参数的assoc捕获输出,并收集所有与之关联的扩展名并检查测试文件的扩展名。
  3. 其他文件扩展名将触发错误"命令未被识别...",因此您可以假设这些文件是不可执行的。

我真的不明白如何区分somefile.py和somefile.txt,因为它们的关联可能是完全相同的。您可以配置系统以与.py文件相同的方式运行.txt文件。


或者您可以通过注册表查找所有具有关联的文件类型。但重点是相同的,任何具有关联的内容都可以直接通过 shell 启动。因此,它几乎涵盖了所有文件,或者您可以手动制作列表。 - Jochen Ritzel
.txt和.py之间的区别在于,将.py放入PATHEXT可能是有意义的。grepath.py的当前版本使用API https://dev59.com/ZEbRa4cB1Zd3GeqP4eLb#1738907执行pp. 1和2。 - jfs
你的答案中的第三点已经在问题中实现了,参见 is_executable_win_destructive() - jfs
据我理解,在 is_executable_win_destructive 函数中,你执行了那个文件。我认为如果你检查了 1 和 2,这是不必要的。剩下的应该只有第 3 点,对吗? - Jiri
@Jiri:第二点可能会以第三点的意义返回不可执行的结果。例如,我曾经看到WinAPI函数之一(不记得是哪一个)返回a.bat~作为可执行文件(第二点),但它在意义上并不可执行(pp. 1或3)。显然,集合1和3不同,这使我们回到原点:如何以非破坏性的方式实现p 3.(稍微简化一下)。 - jfs

2
一个Windows的PE文件总是以"MZ"字符开头。这也包括了一些不一定是可执行文件的DLL文件。
然而,要检查这一点,你需要打开文件并读取文件头,所以这可能不是你想要的。

从技术上讲,DOS .exe 文件以 MZ 开头。PE 文件包含一个 DOS 可执行文件(通常这个程序会打印“此程序需要 Windows。”),然后是以 PE 开头的 PE 头。 - jmucchiello
我已经添加了我认为是可执行文件的示例。我不仅对exe文件感兴趣。 - jfs

1

这是我在问题中链接的grepath.py:

#!/usr/bin/env python
"""Find executables in %PATH% that match PATTERN.

"""
#XXX: remove --use-pathext option

import fnmatch, itertools, os, re, sys, warnings
from optparse import OptionParser
from stat import S_IMODE, S_ISREG, ST_MODE
from subprocess import PIPE, Popen


def warn_import(*args):
    """pass '-Wd' option to python interpreter to see these warnings."""
    warnings.warn("%r" % (args,), ImportWarning, stacklevel=2)


class samefile_win:
    """
http://timgolden.me.uk/python/win32_how_do_i/see_if_two_files_are_the_same_file.html
"""
    @staticmethod
    def get_read_handle (filename):
        return win32file.CreateFile (
            filename,
            win32file.GENERIC_READ,
            win32file.FILE_SHARE_READ,
            None,
            win32file.OPEN_EXISTING,
            0,
            None
            )

    @staticmethod
    def get_unique_id (hFile):
        (attributes,
         created_at, accessed_at, written_at,
         volume,
         file_hi, file_lo,
         n_links,
         index_hi, index_lo
         ) = win32file.GetFileInformationByHandle (hFile)
        return volume, index_hi, index_lo

    @staticmethod
    def samefile_win(filename1, filename2):
        """Whether filename1 and filename2 represent the same file.

It works for subst, ntfs hardlinks, junction points.
It works unreliably for network drives.

Based on GetFileInformationByHandle() Win32 API call.
http://timgolden.me.uk/python/win32_how_do_i/see_if_two_files_are_the_same_file.html
"""
        if samefile_generic(filename1, filename2): return True
        try:
            hFile1 = samefile_win.get_read_handle (filename1)
            hFile2 = samefile_win.get_read_handle (filename2)
            are_equal = (samefile_win.get_unique_id (hFile1)
                         == samefile_win.get_unique_id (hFile2))
            hFile2.Close ()
            hFile1.Close ()
            return are_equal
        except win32file.error:
            return None


def canonical_path(path):
    """NOTE: it might return wrong path for paths with symbolic links."""
    return os.path.realpath(os.path.normcase(path))


def samefile_generic(path1, path2):
    return canonical_path(path1) == canonical_path(path2)


class is_executable_destructive:
    @staticmethod
    def error_message(barename):
        r"""
"'%(barename)s' is not recognized as an internal or external\r\n
command, operable program or batch file.\r\n"

in Russian:
"""
        return '"%(barename)s" \xad\xa5 \xef\xa2\xab\xef\xa5\xe2\xe1\xef \xa2\xad\xe3\xe2\xe0\xa5\xad\xad\xa5\xa9 \xa8\xab\xa8 \xa2\xad\xa5\xe8\xad\xa5\xa9\r\n\xaa\xae\xac\xa0\xad\xa4\xae\xa9, \xa8\xe1\xaf\xae\xab\xad\xef\xa5\xac\xae\xa9 \xaf\xe0\xae\xa3\xe0\xa0\xac\xac\xae\xa9 \xa8\xab\xa8 \xaf\xa0\xaa\xa5\xe2\xad\xeb\xac \xe4\xa0\xa9\xab\xae\xac.\r\n' % dict(barename=barename)

    @staticmethod
    def is_executable_win_destructive(path):
        # assume path <-> barename that is false in general
        barename = os.path.splitext(os.path.basename(path))[0]
        p = Popen(barename, stdout=PIPE, stderr=PIPE, shell=True)
        stdout, stderr = p.communicate()
        return p.poll() != 1 or stdout != '' or stderr != error_message(barename)


def is_executable_win(path):
    """Based on:
http://timgolden.me.uk/python/win32_how_do_i/tell-if-a-file-is-executable.html

Known bugs: treat some "*~" files as executable, e.g. some "*.bat~" files
"""
    try:
        _, executable = FindExecutable(path)
        return bool(samefile(GetLongPathName(executable), path))
    except error:
        return None # not an exe or a document with assoc.


def is_executable_posix(path):
    """Whether the file is executable.

Based on which.py from stdlib
"""
    #XXX it ignores effective uid, guid?
    try: st = os.stat(path)
    except os.error:
        return None

    isregfile = S_ISREG(st[ST_MODE])
    isexemode = (S_IMODE(st[ST_MODE]) & 0111)
    return bool(isregfile and isexemode)

try:
    #XXX replace with ctypes?
    from win32api import FindExecutable, GetLongPathName, error
    is_executable = is_executable_win
except ImportError, e:
    warn_import("is_executable: fall back on posix variant", e)
    is_executable = is_executable_posix

try: samefile = os.path.samefile
except AttributeError, e:
    warn_import("samefile: fallback to samefile_win", e)
    try:
        import win32file
        samefile = samefile_win.samefile_win
    except ImportError, e:
        warn_import("samefile: fallback to generic", e)
        samefile = samefile_generic

def main():
    parser = OptionParser(usage="""
%prog [options] PATTERN
%prog [options] -e PATTERN""", description=__doc__)
    opt = parser.add_option
    opt("-e", "--regex", metavar="PATTERN",
        help="use PATTERN as a regular expression")
    opt("--ignore-case", action="store_true", default=True,
        help="""[default] ignore case when --regex is present; for \
non-regex PATTERN both FILENAME and PATTERN are first \
case-normalized if the operating system requires it otherwise \
unchanged.""")
    opt("--no-ignore-case", dest="ignore_case", action="store_false")
    opt("--use-pathext", action="store_true", default=True,
        help="[default] whether to use %PATHEXT% environment variable")
    opt("--no-use-pathext", dest="use_pathext", action="store_false")
    opt("--show-non-executable", action="store_true", default=False,
        help="show non executable files")

    (options, args) = parser.parse_args()

    if len(args) != 1 and not options.regex:
       parser.error("incorrect number of arguments")
    if not options.regex:
       pattern = args[0]
    del args

    if options.regex:
       filepred = re.compile(options.regex, options.ignore_case and re.I).search
    else:
       fnmatch_ = fnmatch.fnmatch if options.ignore_case else fnmatch.fnmatchcase
       for file_pattern_symbol in "*?":
           if file_pattern_symbol in pattern:
               break
       else: # match in any place if no explicit file pattern symbols supplied
           pattern = "*" + pattern + "*"
       filepred = lambda fn: fnmatch_(fn, pattern)

    if not options.regex and options.ignore_case:
       filter_files = lambda files: fnmatch.filter(files, pattern)
    else:
       filter_files = lambda files: itertools.ifilter(filepred, files)

    if options.use_pathext:
       pathexts = frozenset(map(str.upper,
            os.environ.get('PATHEXT', '').split(os.pathsep)))

    seen = set()
    for dirpath in os.environ.get('PATH', '').split(os.pathsep):
        if os.path.isdir(dirpath): # assume no expansion needed
           # visit "each" directory only once
           # it is unaware of subst drives, junction points, symlinks, etc
           rp = canonical_path(dirpath)
           if rp in seen: continue
           seen.add(rp); del rp

           for filename in filter_files(os.listdir(dirpath)):
               path = os.path.join(dirpath, filename)
               isexe = is_executable(path)

               if isexe == False and is_executable == is_executable_win:
                  # path is a document with associated program
                  # check whether it is a script (.pl, .rb, .py, etc)
                  if not isexe and options.use_pathext:
                     ext = os.path.splitext(path)[1]
                     isexe = ext.upper() in pathexts

               if isexe:
                  print path
               elif options.show_non_executable:
                  print "non-executable:", path


if __name__=="__main__":
   main()

0

解析PE格式。

http://code.google.com/p/pefile/

除了使用Python实际尝试运行程序之外,这可能是您可以得到的最佳解决方案。

编辑:我看到您还想要具有关联的文件。这将需要在注册表中进行操作,但我没有相关信息。

编辑2:我还看到您区分.doc和.py。这是一种相当任意的区分,必须使用手动规则指定,因为对于Windows来说,它们都是程序读取的文件扩展名。


考虑一下:.txt和.bat之间有什么区别?为什么会有%PATHEXT%环境变量? - jfs

-1

无法回答你的问题。Windows无法区分与脚本语言相关联的文件与其他任意程序。对于Windows来说,.PY文件只是由python.exe打开的文档。


请阅读我问题中的“可执行文件是什么”部分。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接