1

プロジェクトの TAGS ファイルを作成した後 ( find . -name "*.py" | xargs etags) M-.、関数の定義にジャンプするために使用できます。それは素晴らしいことです。しかし、グローバル定数の定義が必要な場合、たとえば、x = 3Emacs はそれを見つける場所を知りません。

関数だけでなく、定数が定義されている場所をEmacsに説明する方法はありますか? 関数 (または for ループなど) 内で定義されたものにはこれは必要ありません。グローバルなものだけです。

もっと詳しく

この質問の以前の化身では、「グローバル」ではなく「トップレベル」を使用していましたが、@ Thomas の助けを借りて、それが不正確であることに気付きました。グローバル定義とは、モジュールが定義するものすべてです。したがって、

import m

if m.foo:
  def f():
    x = 3
    return x
  y, z = 1, 2
else:
  def f():
    x = 4
    return x
  y, z = 2, 3
del(z)

これらの定義のサイトが右にインデントされているにもかかわらず、モジュールによって定義されるものはfとです。はローカル変数であり、の定義はモジュールの終了前に削除されます。yxz

すべてのグローバル割り当てをキャプチャするための十分なルールは、式内で単純にそれらを無視しdef(defキーワード自体が任意のレベルでインデントされている可能性があることに注意してください)、それ以外の場合は左側のシンボルを解析する=ことであると思います (複数の可能性があることに注意してください)。 1 つは、Python がタプル割り当てをサポートしているためです)。

4

2 に答える 2

0

もう1つの答えは、インデントのない行のみがグローバル変数宣言を含むと見なします。これにより、関数とクラス定義の本体が効果的に除外されますが、if宣言内で定義されたグローバル変数が失われます。このような宣言は、たとえば、使用する OS によって異なる定数など、珍しいことではありません。

質問の下のコメントで議論されているように、Python の動的な性質により、プログラムが実際に実行されない限り、どの変数がグローバルに定義されているかを完全な精度で決定することが不可能になるため、静的分析は必然的に不完全です。

したがって、以下も単なる概算です。ただし、上記のようにifs 内のグローバル変数定義は考慮されます。これは、ソース ファイルの解析ツリーを実際に分析することによって行うのが最善であるため、bash スクリプトはもはや適切な選択ではありません。ast便利なことに、Python 自体は、ここで使用されているパッケージを介して解析ツリーに簡単にアクセスできます。

from argparse import ArgumentParser, SUPPRESS
import ast
from collections import Counter
from re import match as re_startswith
import os
import subprocess
import sys

# extract variable information from assign statements
def process_assign(target, results):
    if isinstance(target, ast.Name):
        results.append((target.lineno, target.col_offset, target.id))
    elif isinstance(target, ast.Tuple):
        for child in ast.iter_child_nodes(target):
            process_assign(child, results)

# extract variable information from delete statements
def process_delete(target, results):
    if isinstance(target, ast.Name):
        results[:] = filter(lambda t: t[2] != target.id, results)
    elif isinstance(target, ast.Tuple):
        for child in ast.iter_child_nodes(target):
            process_delete(child, results)

# recursively walk the parse tree of the source file
def process_node(node, results):
    if isinstance(node, ast.Assign):
        for target in node.targets:
            process_assign(target, results)
    elif isinstance(node, ast.Delete):
        for target in node.targets:
            process_delete(target, results)
    elif type(node) not in [ast.FunctionDef, ast.ClassDef]:
        for child in ast.iter_child_nodes(node):
            process_node(child, results)

def get_arg_parser():
    # create the parser to configure
    parser = ArgumentParser(usage=SUPPRESS, add_help=False)

    # run etags to find out about the supported command line parameters
    dashlines = list(filter(lambda line: re_startswith('\\s*-', line),
                            subprocess.check_output(['etags', '-h'],
                                                    encoding='utf-8').split('\n')))

    # ignore lines that start with a dash but don't have the right
    # indentation
    most_common_indent = max([(v,k) for k,v in
                              Counter([line.index('-') for line in dashlines]).items()])[1]
    arglines = filter(lambda line: line.index('-') == most_common_indent, dashlines)

    for argline in arglines:
        # the various 'argline' entries contain the command line
        # arguments for etags, sometimes more than one separated by
        # commas.
        for arg in argline.split(','):
            if 'or' in arg:
                arg = arg[:arg.index('or')]
            if ' ' in arg or '=' in arg:
                arg = arg[:min(arg.index(' ') if ' ' in arg else len(arg),
                               arg.index('=') if '=' in arg else len(arg))]
                action='store'
            else:
                action='store_true'
            arg = arg.strip()
            if arg and not (arg == '-h' or arg == '--help'):
                parser.add_argument(arg, action=action)

    # we know we need files to run on
    parser.add_argument('files', nargs='*', metavar='file')

    # the parser is configured now to accept all of etags' arguments
    return parser


if __name__ == '__main__':
    # construct a parser for the command line arguments, unless
    # -h/-help/--help is given in which case we just print the help
    # screen
    etags_args = sys.argv[1:]
    if '-h' in etags_args or '-help' in etags_args or '--help' in etags_args:
        unknown_args = True
    else:
        argparser = get_arg_parser()
        known_ns, unknown_args = argparser.parse_known_args()

    # if something's wrong with the command line arguments, print
    # etags' help screen and exit
    if unknown_args:
        subprocess.run(['etags', '-h'], encoding='utf-8')
        sys.exit(1)

    # we base the output filename on the TAGS file name.  Other than
    # that, we only care about the actual filenames to parse, and all
    # other command line arguments are simply passed to etags later on
    tags_file = 'TAGS2' if hasattr(known_ns, 'o') is None else known_ns.o + '2'
    filenames = known_ns.files

    if filenames:
        # TAGS file sections, one per source file
        sections = []

        # process all files to populate the 'sections' list
        for filename in filenames:
            # read source file
            offsets = [0]; lines = []
            offsets, lines = [0], []
            with open(filename, 'r') as f:
                for line in f.readlines():
                    offsets.append(offsets[-1] + len(bytes(line, 'utf-8')))
                    lines.append(line)

            offsets = offsets[:-1]

            # parse source file
            source = ''.join(lines)
            root_node = ast.parse(source, filename)

            # extract global variable definitions
            vardefs = []
            process_node(root_node, vardefs)

            # create TAGS file section
            sections.append("")
            for lineno, column, varname in vardefs:
                line = lines[lineno-1]
                offset = offsets[lineno-1]
                end = line.index('=') if '=' in line else -1
                sections[-1] += f"{line[:end]}\x7f{varname}\x01{lineno},{offset + column - 1}\n"

        # write TAGS file
        with open(tags_file, 'w') as f:
            for filename, section in zip(filenames, sections):
                if section:
                    f.write("\x0c\n")
                    f.write(filename)
                    f.write(",")
                    f.write(str(len(bytes(section, 'utf-8'))))
                    f.write("\n")
                    f.write(section)
                    f.write("\n")

        # make sure etags includes the newly created file
        etags_args += ['-i', tags_file]

    # now run the actual etags to take care of all other definitions
    try:
        cp = subprocess.run(['etags'] + etags_args, encoding='utf-8')
        status = cp.returncode
    except:
        status = 1

    # if etags did not finish successfully, remove the tags_file
    if status != 0:
        try:
            os.remove(tags_file)
        except FileNotFoundError:
            # nothing to be removed
            pass

etags他の回答と同様に、このスクリプトは標準を内部的に呼び出すため、標準のドロップイン置換を目的としています。したがって、すべてのコマンド ライン パラメータも受け入れetagsます (ただし、現在は考慮されていません-a)。

シェルの init ファイルをエイリアスで修正することをお勧めします。たとえば、次の行を に追加します~/.bashrc

alias etags+=python3 -u /path/to/script.py

/path/to/script.py、上記のコードが保存されたファイルへのパスです。このようなエイリアスがあれば、簡単に呼び出すことができます

etags+ /path/to/file

于 2021-04-13T13:56:06.853 に答える