もう1つの答えは、インデントのない行のみがグローバル変数宣言を含むと見なします。これにより、関数とクラス定義の本体が効果的に除外されますが、if
宣言内で定義されたグローバル変数が失われます。このような宣言は、たとえば、使用する OS によって異なる定数など、珍しいことではありません。
質問の下のコメントで議論されているように、Python の動的な性質により、プログラムが実際に実行されない限り、どの変数がグローバルに定義されているかを完全な精度で決定することが不可能になるため、静的分析は必然的に不完全です。
したがって、以下も単なる概算です。ただし、上記のようにif
s 内のグローバル変数定義は考慮されます。これは、ソース ファイルの解析ツリーを実際に分析することによって行うのが最善であるため、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
等