You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

copyright_header.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2016 The Bitcoin Core developers
  3. # Distributed under the MIT software license, see the accompanying
  4. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  5. import re
  6. import fnmatch
  7. import sys
  8. import subprocess
  9. import datetime
  10. import os
  11. ################################################################################
  12. # file filtering
  13. ################################################################################
  14. EXCLUDE = [
  15. # libsecp256k1:
  16. 'src/secp256k1/include/secp256k1.h',
  17. 'src/secp256k1/include/secp256k1_ecdh.h',
  18. 'src/secp256k1/include/secp256k1_recovery.h',
  19. 'src/secp256k1/include/secp256k1_schnorr.h',
  20. 'src/secp256k1/src/java/org_bitcoin_NativeSecp256k1.c',
  21. 'src/secp256k1/src/java/org_bitcoin_NativeSecp256k1.h',
  22. 'src/secp256k1/src/java/org_bitcoin_Secp256k1Context.c',
  23. 'src/secp256k1/src/java/org_bitcoin_Secp256k1Context.h',
  24. # auto generated:
  25. 'src/univalue/lib/univalue_escapes.h',
  26. 'src/qt/bitcoinstrings.cpp',
  27. 'src/chainparamsseeds.h',
  28. # other external copyrights:
  29. 'src/tinyformat.h',
  30. 'src/leveldb/util/env_win.cc',
  31. 'src/crypto/ctaes/bench.c',
  32. 'test/functional/test_framework/bignum.py',
  33. # python init:
  34. '*__init__.py',
  35. ]
  36. EXCLUDE_COMPILED = re.compile('|'.join([fnmatch.translate(m) for m in EXCLUDE]))
  37. INCLUDE = ['*.h', '*.cpp', '*.cc', '*.c', '*.py']
  38. INCLUDE_COMPILED = re.compile('|'.join([fnmatch.translate(m) for m in INCLUDE]))
  39. def applies_to_file(filename):
  40. return ((EXCLUDE_COMPILED.match(filename) is None) and
  41. (INCLUDE_COMPILED.match(filename) is not None))
  42. ################################################################################
  43. # obtain list of files in repo according to INCLUDE and EXCLUDE
  44. ################################################################################
  45. GIT_LS_CMD = 'git ls-files'
  46. def call_git_ls():
  47. out = subprocess.check_output(GIT_LS_CMD.split(' '))
  48. return [f for f in out.decode("utf-8").split('\n') if f != '']
  49. def get_filenames_to_examine():
  50. filenames = call_git_ls()
  51. return sorted([filename for filename in filenames if
  52. applies_to_file(filename)])
  53. ################################################################################
  54. # define and compile regexes for the patterns we are looking for
  55. ################################################################################
  56. COPYRIGHT_WITH_C = 'Copyright \(c\)'
  57. COPYRIGHT_WITHOUT_C = 'Copyright'
  58. ANY_COPYRIGHT_STYLE = '(%s|%s)' % (COPYRIGHT_WITH_C, COPYRIGHT_WITHOUT_C)
  59. YEAR = "20[0-9][0-9]"
  60. YEAR_RANGE = '(%s)(-%s)?' % (YEAR, YEAR)
  61. YEAR_LIST = '(%s)(, %s)+' % (YEAR, YEAR)
  62. ANY_YEAR_STYLE = '(%s|%s)' % (YEAR_RANGE, YEAR_LIST)
  63. ANY_COPYRIGHT_STYLE_OR_YEAR_STYLE = ("%s %s" % (ANY_COPYRIGHT_STYLE,
  64. ANY_YEAR_STYLE))
  65. ANY_COPYRIGHT_COMPILED = re.compile(ANY_COPYRIGHT_STYLE_OR_YEAR_STYLE)
  66. def compile_copyright_regex(copyright_style, year_style, name):
  67. return re.compile('%s %s %s' % (copyright_style, year_style, name))
  68. EXPECTED_HOLDER_NAMES = [
  69. "Satoshi Nakamoto\n",
  70. "The Bitcoin Core developers\n",
  71. "The Bitcoin Core developers \n",
  72. "Bitcoin Core Developers\n",
  73. "the Bitcoin Core developers\n",
  74. "The Bitcoin developers\n",
  75. "The LevelDB Authors\. All rights reserved\.\n",
  76. "BitPay Inc\.\n",
  77. "BitPay, Inc\.\n",
  78. "University of Illinois at Urbana-Champaign\.\n",
  79. "MarcoFalke\n",
  80. "Pieter Wuille\n",
  81. "Pieter Wuille +\*\n",
  82. "Pieter Wuille, Gregory Maxwell +\*\n",
  83. "Pieter Wuille, Andrew Poelstra +\*\n",
  84. "Andrew Poelstra +\*\n",
  85. "Wladimir J. van der Laan\n",
  86. "Jeff Garzik\n",
  87. "Diederik Huys, Pieter Wuille +\*\n",
  88. "Thomas Daede, Cory Fields +\*\n",
  89. "Jan-Klaas Kollhof\n",
  90. "Sam Rushing\n",
  91. "ArtForz -- public domain half-a-node\n",
  92. ]
  93. DOMINANT_STYLE_COMPILED = {}
  94. YEAR_LIST_STYLE_COMPILED = {}
  95. WITHOUT_C_STYLE_COMPILED = {}
  96. for holder_name in EXPECTED_HOLDER_NAMES:
  97. DOMINANT_STYLE_COMPILED[holder_name] = (
  98. compile_copyright_regex(COPYRIGHT_WITH_C, YEAR_RANGE, holder_name))
  99. YEAR_LIST_STYLE_COMPILED[holder_name] = (
  100. compile_copyright_regex(COPYRIGHT_WITH_C, YEAR_LIST, holder_name))
  101. WITHOUT_C_STYLE_COMPILED[holder_name] = (
  102. compile_copyright_regex(COPYRIGHT_WITHOUT_C, ANY_YEAR_STYLE,
  103. holder_name))
  104. ################################################################################
  105. # search file contents for copyright message of particular category
  106. ################################################################################
  107. def get_count_of_copyrights_of_any_style_any_holder(contents):
  108. return len(ANY_COPYRIGHT_COMPILED.findall(contents))
  109. def file_has_dominant_style_copyright_for_holder(contents, holder_name):
  110. match = DOMINANT_STYLE_COMPILED[holder_name].search(contents)
  111. return match is not None
  112. def file_has_year_list_style_copyright_for_holder(contents, holder_name):
  113. match = YEAR_LIST_STYLE_COMPILED[holder_name].search(contents)
  114. return match is not None
  115. def file_has_without_c_style_copyright_for_holder(contents, holder_name):
  116. match = WITHOUT_C_STYLE_COMPILED[holder_name].search(contents)
  117. return match is not None
  118. ################################################################################
  119. # get file info
  120. ################################################################################
  121. def read_file(filename):
  122. return open(os.path.abspath(filename), 'r').read()
  123. def gather_file_info(filename):
  124. info = {}
  125. info['filename'] = filename
  126. c = read_file(filename)
  127. info['contents'] = c
  128. info['all_copyrights'] = get_count_of_copyrights_of_any_style_any_holder(c)
  129. info['classified_copyrights'] = 0
  130. info['dominant_style'] = {}
  131. info['year_list_style'] = {}
  132. info['without_c_style'] = {}
  133. for holder_name in EXPECTED_HOLDER_NAMES:
  134. has_dominant_style = (
  135. file_has_dominant_style_copyright_for_holder(c, holder_name))
  136. has_year_list_style = (
  137. file_has_year_list_style_copyright_for_holder(c, holder_name))
  138. has_without_c_style = (
  139. file_has_without_c_style_copyright_for_holder(c, holder_name))
  140. info['dominant_style'][holder_name] = has_dominant_style
  141. info['year_list_style'][holder_name] = has_year_list_style
  142. info['without_c_style'][holder_name] = has_without_c_style
  143. if has_dominant_style or has_year_list_style or has_without_c_style:
  144. info['classified_copyrights'] = info['classified_copyrights'] + 1
  145. return info
  146. ################################################################################
  147. # report execution
  148. ################################################################################
  149. SEPARATOR = '-'.join(['' for _ in range(80)])
  150. def print_filenames(filenames, verbose):
  151. if not verbose:
  152. return
  153. for filename in filenames:
  154. print("\t%s" % filename)
  155. def print_report(file_infos, verbose):
  156. print(SEPARATOR)
  157. examined = [i['filename'] for i in file_infos]
  158. print("%d files examined according to INCLUDE and EXCLUDE fnmatch rules" %
  159. len(examined))
  160. print_filenames(examined, verbose)
  161. print(SEPARATOR)
  162. print('')
  163. zero_copyrights = [i['filename'] for i in file_infos if
  164. i['all_copyrights'] == 0]
  165. print("%4d with zero copyrights" % len(zero_copyrights))
  166. print_filenames(zero_copyrights, verbose)
  167. one_copyright = [i['filename'] for i in file_infos if
  168. i['all_copyrights'] == 1]
  169. print("%4d with one copyright" % len(one_copyright))
  170. print_filenames(one_copyright, verbose)
  171. two_copyrights = [i['filename'] for i in file_infos if
  172. i['all_copyrights'] == 2]
  173. print("%4d with two copyrights" % len(two_copyrights))
  174. print_filenames(two_copyrights, verbose)
  175. three_copyrights = [i['filename'] for i in file_infos if
  176. i['all_copyrights'] == 3]
  177. print("%4d with three copyrights" % len(three_copyrights))
  178. print_filenames(three_copyrights, verbose)
  179. four_or_more_copyrights = [i['filename'] for i in file_infos if
  180. i['all_copyrights'] >= 4]
  181. print("%4d with four or more copyrights" % len(four_or_more_copyrights))
  182. print_filenames(four_or_more_copyrights, verbose)
  183. print('')
  184. print(SEPARATOR)
  185. print('Copyrights with dominant style:\ne.g. "Copyright (c)" and '
  186. '"<year>" or "<startYear>-<endYear>":\n')
  187. for holder_name in EXPECTED_HOLDER_NAMES:
  188. dominant_style = [i['filename'] for i in file_infos if
  189. i['dominant_style'][holder_name]]
  190. if len(dominant_style) > 0:
  191. print("%4d with '%s'" % (len(dominant_style),
  192. holder_name.replace('\n', '\\n')))
  193. print_filenames(dominant_style, verbose)
  194. print('')
  195. print(SEPARATOR)
  196. print('Copyrights with year list style:\ne.g. "Copyright (c)" and '
  197. '"<year1>, <year2>, ...":\n')
  198. for holder_name in EXPECTED_HOLDER_NAMES:
  199. year_list_style = [i['filename'] for i in file_infos if
  200. i['year_list_style'][holder_name]]
  201. if len(year_list_style) > 0:
  202. print("%4d with '%s'" % (len(year_list_style),
  203. holder_name.replace('\n', '\\n')))
  204. print_filenames(year_list_style, verbose)
  205. print('')
  206. print(SEPARATOR)
  207. print('Copyrights with no "(c)" style:\ne.g. "Copyright" and "<year>" or '
  208. '"<startYear>-<endYear>":\n')
  209. for holder_name in EXPECTED_HOLDER_NAMES:
  210. without_c_style = [i['filename'] for i in file_infos if
  211. i['without_c_style'][holder_name]]
  212. if len(without_c_style) > 0:
  213. print("%4d with '%s'" % (len(without_c_style),
  214. holder_name.replace('\n', '\\n')))
  215. print_filenames(without_c_style, verbose)
  216. print('')
  217. print(SEPARATOR)
  218. unclassified_copyrights = [i['filename'] for i in file_infos if
  219. i['classified_copyrights'] < i['all_copyrights']]
  220. print("%d with unexpected copyright holder names" %
  221. len(unclassified_copyrights))
  222. print_filenames(unclassified_copyrights, verbose)
  223. print(SEPARATOR)
  224. def exec_report(base_directory, verbose):
  225. original_cwd = os.getcwd()
  226. os.chdir(base_directory)
  227. filenames = get_filenames_to_examine()
  228. file_infos = [gather_file_info(f) for f in filenames]
  229. print_report(file_infos, verbose)
  230. os.chdir(original_cwd)
  231. ################################################################################
  232. # report cmd
  233. ################################################################################
  234. REPORT_USAGE = """
  235. Produces a report of all copyright header notices found inside the source files
  236. of a repository.
  237. Usage:
  238. $ ./copyright_header.py report <base_directory> [verbose]
  239. Arguments:
  240. <base_directory> - The base directory of a bitcoin source code repository.
  241. [verbose] - Includes a list of every file of each subcategory in the report.
  242. """
  243. def report_cmd(argv):
  244. if len(argv) == 2:
  245. sys.exit(REPORT_USAGE)
  246. base_directory = argv[2]
  247. if not os.path.exists(base_directory):
  248. sys.exit("*** bad <base_directory>: %s" % base_directory)
  249. if len(argv) == 3:
  250. verbose = False
  251. elif argv[3] == 'verbose':
  252. verbose = True
  253. else:
  254. sys.exit("*** unknown argument: %s" % argv[2])
  255. exec_report(base_directory, verbose)
  256. ################################################################################
  257. # query git for year of last change
  258. ################################################################################
  259. GIT_LOG_CMD = "git log --pretty=format:%%ai %s"
  260. def call_git_log(filename):
  261. out = subprocess.check_output((GIT_LOG_CMD % filename).split(' '))
  262. return out.decode("utf-8").split('\n')
  263. def get_git_change_years(filename):
  264. git_log_lines = call_git_log(filename)
  265. if len(git_log_lines) == 0:
  266. return [datetime.date.today().year]
  267. # timestamp is in ISO 8601 format. e.g. "2016-09-05 14:25:32 -0600"
  268. return [line.split(' ')[0].split('-')[0] for line in git_log_lines]
  269. def get_most_recent_git_change_year(filename):
  270. return max(get_git_change_years(filename))
  271. ################################################################################
  272. # read and write to file
  273. ################################################################################
  274. def read_file_lines(filename):
  275. f = open(os.path.abspath(filename), 'r')
  276. file_lines = f.readlines()
  277. f.close()
  278. return file_lines
  279. def write_file_lines(filename, file_lines):
  280. f = open(os.path.abspath(filename), 'w')
  281. f.write(''.join(file_lines))
  282. f.close()
  283. ################################################################################
  284. # update header years execution
  285. ################################################################################
  286. COPYRIGHT = 'Copyright \(c\)'
  287. YEAR = "20[0-9][0-9]"
  288. YEAR_RANGE = '(%s)(-%s)?' % (YEAR, YEAR)
  289. HOLDER = 'The Bitcoin Core developers'
  290. UPDATEABLE_LINE_COMPILED = re.compile(' '.join([COPYRIGHT, YEAR_RANGE, HOLDER]))
  291. def get_updatable_copyright_line(file_lines):
  292. index = 0
  293. for line in file_lines:
  294. if UPDATEABLE_LINE_COMPILED.search(line) is not None:
  295. return index, line
  296. index = index + 1
  297. return None, None
  298. def parse_year_range(year_range):
  299. year_split = year_range.split('-')
  300. start_year = year_split[0]
  301. if len(year_split) == 1:
  302. return start_year, start_year
  303. return start_year, year_split[1]
  304. def year_range_to_str(start_year, end_year):
  305. if start_year == end_year:
  306. return start_year
  307. return "%s-%s" % (start_year, end_year)
  308. def create_updated_copyright_line(line, last_git_change_year):
  309. copyright_splitter = 'Copyright (c) '
  310. copyright_split = line.split(copyright_splitter)
  311. # Preserve characters on line that are ahead of the start of the copyright
  312. # notice - they are part of the comment block and vary from file-to-file.
  313. before_copyright = copyright_split[0]
  314. after_copyright = copyright_split[1]
  315. space_split = after_copyright.split(' ')
  316. year_range = space_split[0]
  317. start_year, end_year = parse_year_range(year_range)
  318. if end_year == last_git_change_year:
  319. return line
  320. return (before_copyright + copyright_splitter +
  321. year_range_to_str(start_year, last_git_change_year) + ' ' +
  322. ' '.join(space_split[1:]))
  323. def update_updatable_copyright(filename):
  324. file_lines = read_file_lines(filename)
  325. index, line = get_updatable_copyright_line(file_lines)
  326. if not line:
  327. print_file_action_message(filename, "No updatable copyright.")
  328. return
  329. last_git_change_year = get_most_recent_git_change_year(filename)
  330. new_line = create_updated_copyright_line(line, last_git_change_year)
  331. if line == new_line:
  332. print_file_action_message(filename, "Copyright up-to-date.")
  333. return
  334. file_lines[index] = new_line
  335. write_file_lines(filename, file_lines)
  336. print_file_action_message(filename,
  337. "Copyright updated! -> %s" % last_git_change_year)
  338. def exec_update_header_year(base_directory):
  339. original_cwd = os.getcwd()
  340. os.chdir(base_directory)
  341. for filename in get_filenames_to_examine():
  342. update_updatable_copyright(filename)
  343. os.chdir(original_cwd)
  344. ################################################################################
  345. # update cmd
  346. ################################################################################
  347. UPDATE_USAGE = """
  348. Updates all the copyright headers of "The Bitcoin Core developers" which were
  349. changed in a year more recent than is listed. For example:
  350. // Copyright (c) <firstYear>-<lastYear> The Bitcoin Core developers
  351. will be updated to:
  352. // Copyright (c) <firstYear>-<lastModifiedYear> The Bitcoin Core developers
  353. where <lastModifiedYear> is obtained from the 'git log' history.
  354. This subcommand also handles copyright headers that have only a single year. In those cases:
  355. // Copyright (c) <year> The Bitcoin Core developers
  356. will be updated to:
  357. // Copyright (c) <year>-<lastModifiedYear> The Bitcoin Core developers
  358. where the update is appropriate.
  359. Usage:
  360. $ ./copyright_header.py update <base_directory>
  361. Arguments:
  362. <base_directory> - The base directory of a bitcoin source code repository.
  363. """
  364. def print_file_action_message(filename, action):
  365. print("%-52s %s" % (filename, action))
  366. def update_cmd(argv):
  367. if len(argv) != 3:
  368. sys.exit(UPDATE_USAGE)
  369. base_directory = argv[2]
  370. if not os.path.exists(base_directory):
  371. sys.exit("*** bad base_directory: %s" % base_directory)
  372. exec_update_header_year(base_directory)
  373. ################################################################################
  374. # inserted copyright header format
  375. ################################################################################
  376. def get_header_lines(header, start_year, end_year):
  377. lines = header.split('\n')[1:-1]
  378. lines[0] = lines[0] % year_range_to_str(start_year, end_year)
  379. return [line + '\n' for line in lines]
  380. CPP_HEADER = '''
  381. // Copyright (c) %s The Bitcoin Core developers
  382. // Distributed under the MIT software license, see the accompanying
  383. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  384. '''
  385. def get_cpp_header_lines_to_insert(start_year, end_year):
  386. return reversed(get_header_lines(CPP_HEADER, start_year, end_year))
  387. PYTHON_HEADER = '''
  388. # Copyright (c) %s The Bitcoin Core developers
  389. # Distributed under the MIT software license, see the accompanying
  390. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  391. '''
  392. def get_python_header_lines_to_insert(start_year, end_year):
  393. return reversed(get_header_lines(PYTHON_HEADER, start_year, end_year))
  394. ################################################################################
  395. # query git for year of last change
  396. ################################################################################
  397. def get_git_change_year_range(filename):
  398. years = get_git_change_years(filename)
  399. return min(years), max(years)
  400. ################################################################################
  401. # check for existing core copyright
  402. ################################################################################
  403. def file_already_has_core_copyright(file_lines):
  404. index, _ = get_updatable_copyright_line(file_lines)
  405. return index != None
  406. ################################################################################
  407. # insert header execution
  408. ################################################################################
  409. def file_has_hashbang(file_lines):
  410. if len(file_lines) < 1:
  411. return False
  412. if len(file_lines[0]) <= 2:
  413. return False
  414. return file_lines[0][:2] == '#!'
  415. def insert_python_header(filename, file_lines, start_year, end_year):
  416. if file_has_hashbang(file_lines):
  417. insert_idx = 1
  418. else:
  419. insert_idx = 0
  420. header_lines = get_python_header_lines_to_insert(start_year, end_year)
  421. for line in header_lines:
  422. file_lines.insert(insert_idx, line)
  423. write_file_lines(filename, file_lines)
  424. def insert_cpp_header(filename, file_lines, start_year, end_year):
  425. header_lines = get_cpp_header_lines_to_insert(start_year, end_year)
  426. for line in header_lines:
  427. file_lines.insert(0, line)
  428. write_file_lines(filename, file_lines)
  429. def exec_insert_header(filename, style):
  430. file_lines = read_file_lines(filename)
  431. if file_already_has_core_copyright(file_lines):
  432. sys.exit('*** %s already has a copyright by The Bitcoin Core developers'
  433. % (filename))
  434. start_year, end_year = get_git_change_year_range(filename)
  435. if style == 'python':
  436. insert_python_header(filename, file_lines, start_year, end_year)
  437. else:
  438. insert_cpp_header(filename, file_lines, start_year, end_year)
  439. ################################################################################
  440. # insert cmd
  441. ################################################################################
  442. INSERT_USAGE = """
  443. Inserts a copyright header for "The Bitcoin Core developers" at the top of the
  444. file in either Python or C++ style as determined by the file extension. If the
  445. file is a Python file and it has a '#!' starting the first line, the header is
  446. inserted in the line below it.
  447. The copyright dates will be set to be:
  448. "<year_introduced>-<current_year>"
  449. where <year_introduced> is according to the 'git log' history. If
  450. <year_introduced> is equal to <current_year>, the date will be set to be:
  451. "<current_year>"
  452. If the file already has a copyright for "The Bitcoin Core developers", the
  453. script will exit.
  454. Usage:
  455. $ ./copyright_header.py insert <file>
  456. Arguments:
  457. <file> - A source file in the bitcoin repository.
  458. """
  459. def insert_cmd(argv):
  460. if len(argv) != 3:
  461. sys.exit(INSERT_USAGE)
  462. filename = argv[2]
  463. if not os.path.isfile(filename):
  464. sys.exit("*** bad filename: %s" % filename)
  465. _, extension = os.path.splitext(filename)
  466. if extension not in ['.h', '.cpp', '.cc', '.c', '.py']:
  467. sys.exit("*** cannot insert for file extension %s" % extension)
  468. if extension == '.py':
  469. style = 'python'
  470. else:
  471. style = 'cpp'
  472. exec_insert_header(filename, style)
  473. ################################################################################
  474. # UI
  475. ################################################################################
  476. USAGE = """
  477. copyright_header.py - utilities for managing copyright headers of 'The Bitcoin
  478. Core developers' in repository source files.
  479. Usage:
  480. $ ./copyright_header <subcommand>
  481. Subcommands:
  482. report
  483. update
  484. insert
  485. To see subcommand usage, run them without arguments.
  486. """
  487. SUBCOMMANDS = ['report', 'update', 'insert']
  488. if __name__ == "__main__":
  489. if len(sys.argv) == 1:
  490. sys.exit(USAGE)
  491. subcommand = sys.argv[1]
  492. if subcommand not in SUBCOMMANDS:
  493. sys.exit(USAGE)
  494. if subcommand == 'report':
  495. report_cmd(sys.argv)
  496. elif subcommand == 'update':
  497. update_cmd(sys.argv)
  498. elif subcommand == 'insert':
  499. insert_cmd(sys.argv)