1#!/usr/bin/env python3 2# 3#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 4# 5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6# See https://llvm.org/LICENSE.txt for license information. 7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8# 9#===------------------------------------------------------------------------===# 10 11r""" 12clang-format git integration 13============================ 14 15This file provides a clang-format integration for git. Put it somewhere in your 16path and ensure that it is executable. Then, "git clang-format" will invoke 17clang-format on the changes in current files or a specific commit. 18 19For further details, run: 20git clang-format -h 21 22Requires Python 2.7 or Python 3 23""" 24 25from __future__ import absolute_import, division, print_function 26import argparse 27import collections 28import contextlib 29import errno 30import os 31import re 32import subprocess 33import sys 34 35usage = ('git clang-format [OPTIONS] [<commit>] [<commit>|--staged] ' 36 '[--] [<file>...]') 37 38desc = ''' 39If zero or one commits are given, run clang-format on all lines that differ 40between the working directory and <commit>, which defaults to HEAD. Changes are 41only applied to the working directory, or in the stage/index. 42 43Examples: 44 To format staged changes, i.e everything that's been `git add`ed: 45 git clang-format 46 47 To also format everything touched in the most recent commit: 48 git clang-format HEAD~1 49 50 If you're on a branch off main, to format everything touched on your branch: 51 git clang-format main 52 53If two commits are given (requires --diff), run clang-format on all lines in the 54second <commit> that differ from the first <commit>. 55 56The following git-config settings set the default of the corresponding option: 57 clangFormat.binary 58 clangFormat.commit 59 clangFormat.extensions 60 clangFormat.style 61''' 62 63# Name of the temporary index file in which save the output of clang-format. 64# This file is created within the .git directory. 65temp_index_basename = 'clang-format-index' 66 67 68Range = collections.namedtuple('Range', 'start, count') 69 70 71def main(): 72 config = load_git_config() 73 74 # In order to keep '--' yet allow options after positionals, we need to 75 # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 76 # nargs=argparse.REMAINDER disallows options after positionals.) 77 argv = sys.argv[1:] 78 try: 79 idx = argv.index('--') 80 except ValueError: 81 dash_dash = [] 82 else: 83 dash_dash = argv[idx:] 84 argv = argv[:idx] 85 86 default_extensions = ','.join([ 87 # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 88 'c', 'h', # C 89 'm', # ObjC 90 'mm', # ObjC++ 91 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hh', 'hpp', 'hxx', 'inc', # C++ 92 'ccm', 'cppm', 'cxxm', 'c++m', # C++ Modules 93 'cu', 'cuh', # CUDA 94 # Other languages that clang-format supports 95 'proto', 'protodevel', # Protocol Buffers 96 'java', # Java 97 'js', 'mjs', 'cjs', # JavaScript 98 'ts', # TypeScript 99 'cs', # C Sharp 100 'json', # Json 101 'sv', 'svh', 'v', 'vh', # Verilog 102 'td', # TableGen 103 'txtpb', 'textpb', 'pb.txt', 'textproto', 'asciipb', # TextProto 104 ]) 105 106 p = argparse.ArgumentParser( 107 usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 108 description=desc) 109 p.add_argument('--binary', 110 default=config.get('clangformat.binary', 'clang-format'), 111 help='path to clang-format'), 112 p.add_argument('--commit', 113 default=config.get('clangformat.commit', 'HEAD'), 114 help='default commit to use if none is specified'), 115 p.add_argument('--diff', action='store_true', 116 help='print a diff instead of applying the changes') 117 p.add_argument('--diffstat', action='store_true', 118 help='print a diffstat instead of applying the changes') 119 p.add_argument('--extensions', 120 default=config.get('clangformat.extensions', 121 default_extensions), 122 help=('comma-separated list of file extensions to format, ' 123 'excluding the period and case-insensitive')), 124 p.add_argument('-f', '--force', action='store_true', 125 help='allow changes to unstaged files') 126 p.add_argument('-p', '--patch', action='store_true', 127 help='select hunks interactively') 128 p.add_argument('-q', '--quiet', action='count', default=0, 129 help='print less information') 130 p.add_argument('--staged', '--cached', action='store_true', 131 help='format lines in the stage instead of the working dir') 132 p.add_argument('--style', 133 default=config.get('clangformat.style', None), 134 help='passed to clang-format'), 135 p.add_argument('-v', '--verbose', action='count', default=0, 136 help='print extra information') 137 p.add_argument('--diff_from_common_commit', action='store_true', 138 help=('diff from the last common commit for commits in ' 139 'separate branches rather than the exact point of the ' 140 'commits')) 141 # We gather all the remaining positional arguments into 'args' since we need 142 # to use some heuristics to determine whether or not <commit> was present. 143 # However, to print pretty messages, we make use of metavar and help. 144 p.add_argument('args', nargs='*', metavar='<commit>', 145 help='revision from which to compute the diff') 146 p.add_argument('ignored', nargs='*', metavar='<file>...', 147 help='if specified, only consider differences in these files') 148 opts = p.parse_args(argv) 149 150 opts.verbose -= opts.quiet 151 del opts.quiet 152 153 commits, files = interpret_args(opts.args, dash_dash, opts.commit) 154 if len(commits) > 2: 155 die('at most two commits allowed; %d given' % len(commits)) 156 if len(commits) == 2: 157 if opts.staged: 158 die('--staged is not allowed when two commits are given') 159 if not opts.diff: 160 die('--diff is required when two commits are given') 161 elif opts.diff_from_common_commit: 162 die('--diff_from_common_commit is only allowed when two commits are given') 163 164 if os.path.dirname(opts.binary): 165 opts.binary = os.path.abspath(opts.binary) 166 167 changed_lines = compute_diff_and_extract_lines(commits, 168 files, 169 opts.staged, 170 opts.diff_from_common_commit) 171 if opts.verbose >= 1: 172 ignored_files = set(changed_lines) 173 filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 174 # The computed diff outputs absolute paths, so we must cd before accessing 175 # those files. 176 cd_to_toplevel() 177 filter_symlinks(changed_lines) 178 filter_ignored_files(changed_lines, binary=opts.binary) 179 if opts.verbose >= 1: 180 ignored_files.difference_update(changed_lines) 181 if ignored_files: 182 print('Ignoring the following files (wrong extension, symlink, or ' 183 'ignored by clang-format):') 184 for filename in ignored_files: 185 print(' %s' % filename) 186 if changed_lines: 187 print('Running clang-format on the following files:') 188 for filename in changed_lines: 189 print(' %s' % filename) 190 191 if not changed_lines: 192 if opts.verbose >= 0: 193 print('no modified files to format') 194 return 0 195 196 if len(commits) > 1: 197 old_tree = commits[1] 198 revision = old_tree 199 elif opts.staged: 200 old_tree = create_tree_from_index(changed_lines) 201 revision = '' 202 else: 203 old_tree = create_tree_from_workdir(changed_lines) 204 revision = None 205 new_tree = run_clang_format_and_save_to_tree(changed_lines, 206 revision, 207 binary=opts.binary, 208 style=opts.style) 209 if opts.verbose >= 1: 210 print('old tree: %s' % old_tree) 211 print('new tree: %s' % new_tree) 212 213 if old_tree == new_tree: 214 if opts.verbose >= 0: 215 print('clang-format did not modify any files') 216 return 0 217 218 if opts.diff: 219 return print_diff(old_tree, new_tree) 220 if opts.diffstat: 221 return print_diffstat(old_tree, new_tree) 222 223 changed_files = apply_changes(old_tree, new_tree, force=opts.force, 224 patch_mode=opts.patch) 225 if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 226 print('changed files:') 227 for filename in changed_files: 228 print(' %s' % filename) 229 230 return 1 231 232 233def load_git_config(non_string_options=None): 234 """Return the git configuration as a dictionary. 235 236 All options are assumed to be strings unless in `non_string_options`, in which 237 is a dictionary mapping option name (in lower case) to either "--bool" or 238 "--int".""" 239 if non_string_options is None: 240 non_string_options = {} 241 out = {} 242 for entry in run('git', 'config', '--list', '--null').split('\0'): 243 if entry: 244 if '\n' in entry: 245 name, value = entry.split('\n', 1) 246 else: 247 # A setting with no '=' ('\n' with --null) is implicitly 'true' 248 name = entry 249 value = 'true' 250 if name in non_string_options: 251 value = run('git', 'config', non_string_options[name], name) 252 out[name] = value 253 return out 254 255 256def interpret_args(args, dash_dash, default_commit): 257 """Interpret `args` as "[commits] [--] [files]" and return (commits, files). 258 259 It is assumed that "--" and everything that follows has been removed from 260 args and placed in `dash_dash`. 261 262 If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its 263 left (if present) are taken as commits. Otherwise, the arguments are checked 264 from left to right if they are commits or files. If commits are not given, 265 a list with `default_commit` is used.""" 266 if dash_dash: 267 if len(args) == 0: 268 commits = [default_commit] 269 else: 270 commits = args 271 for commit in commits: 272 object_type = get_object_type(commit) 273 if object_type not in ('commit', 'tag'): 274 if object_type is None: 275 die("'%s' is not a commit" % commit) 276 else: 277 die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 278 files = dash_dash[1:] 279 elif args: 280 commits = [] 281 while args: 282 if not disambiguate_revision(args[0]): 283 break 284 commits.append(args.pop(0)) 285 if not commits: 286 commits = [default_commit] 287 files = args 288 else: 289 commits = [default_commit] 290 files = [] 291 return commits, files 292 293 294def disambiguate_revision(value): 295 """Returns True if `value` is a revision, False if it is a file, or dies.""" 296 # If `value` is ambiguous (neither a commit nor a file), the following 297 # command will die with an appropriate error message. 298 run('git', 'rev-parse', value, verbose=False) 299 object_type = get_object_type(value) 300 if object_type is None: 301 return False 302 if object_type in ('commit', 'tag'): 303 return True 304 die('`%s` is a %s, but a commit or filename was expected' % 305 (value, object_type)) 306 307 308def get_object_type(value): 309 """Returns a string description of an object's type, or None if it is not 310 a valid git object.""" 311 cmd = ['git', 'cat-file', '-t', value] 312 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 313 stdout, stderr = p.communicate() 314 if p.returncode != 0: 315 return None 316 return convert_string(stdout.strip()) 317 318 319def compute_diff_and_extract_lines(commits, files, staged, diff_common_commit): 320 """Calls compute_diff() followed by extract_lines().""" 321 diff_process = compute_diff(commits, files, staged, diff_common_commit) 322 changed_lines = extract_lines(diff_process.stdout) 323 diff_process.stdout.close() 324 diff_process.wait() 325 if diff_process.returncode != 0: 326 # Assume error was already printed to stderr. 327 sys.exit(2) 328 return changed_lines 329 330 331def compute_diff(commits, files, staged, diff_common_commit): 332 """Return a subprocess object producing the diff from `commits`. 333 334 The return value's `stdin` file object will produce a patch with the 335 differences between the working directory (or stage if --staged is used) and 336 the first commit if a single one was specified, or the difference between 337 both specified commits, filtered on `files` (if non-empty). 338 Zero context lines are used in the patch.""" 339 git_tool = 'diff-index' 340 extra_args = [] 341 if len(commits) == 2: 342 git_tool = 'diff-tree' 343 if diff_common_commit: 344 commits = [f'{commits[0]}...{commits[1]}'] 345 elif staged: 346 extra_args += ['--cached'] 347 348 cmd = ['git', git_tool, '-p', '-U0'] + extra_args + commits + ['--'] 349 cmd.extend(files) 350 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 351 p.stdin.close() 352 return p 353 354 355def extract_lines(patch_file): 356 """Extract the changed lines in `patch_file`. 357 358 The return value is a dictionary mapping filename to a list of (start_line, 359 line_count) pairs. 360 361 The input must have been produced with ``-U0``, meaning unidiff format with 362 zero lines of context. The return value is a dict mapping filename to a 363 list of line `Range`s.""" 364 matches = {} 365 for line in patch_file: 366 line = convert_string(line) 367 match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 368 if match: 369 filename = match.group(1).rstrip('\r\n\t') 370 match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 371 if match: 372 start_line = int(match.group(1)) 373 line_count = 1 374 if match.group(3): 375 line_count = int(match.group(3)) 376 if line_count == 0: 377 line_count = 1 378 if start_line == 0: 379 continue 380 matches.setdefault(filename, []).append(Range(start_line, line_count)) 381 return matches 382 383 384def filter_by_extension(dictionary, allowed_extensions): 385 """Delete every key in `dictionary` that doesn't have an allowed extension. 386 387 `allowed_extensions` must be a collection of lowercase file extensions, 388 excluding the period.""" 389 allowed_extensions = frozenset(allowed_extensions) 390 for filename in list(dictionary.keys()): 391 base_ext = filename.rsplit('.', 1) 392 if len(base_ext) == 1 and '' in allowed_extensions: 393 continue 394 if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 395 del dictionary[filename] 396 397 398def filter_symlinks(dictionary): 399 """Delete every key in `dictionary` that is a symlink.""" 400 for filename in list(dictionary.keys()): 401 if os.path.islink(filename): 402 del dictionary[filename] 403 404 405def filter_ignored_files(dictionary, binary): 406 """Delete every key in `dictionary` that is ignored by clang-format.""" 407 ignored_files = run(binary, '-list-ignored', *dictionary.keys()) 408 if not ignored_files: 409 return 410 ignored_files = ignored_files.split('\n') 411 for filename in ignored_files: 412 del dictionary[filename] 413 414 415def cd_to_toplevel(): 416 """Change to the top level of the git repository.""" 417 toplevel = run('git', 'rev-parse', '--show-toplevel') 418 os.chdir(toplevel) 419 420 421def create_tree_from_workdir(filenames): 422 """Create a new git tree with the given files from the working directory. 423 424 Returns the object ID (SHA-1) of the created tree.""" 425 return create_tree(filenames, '--stdin') 426 427 428def create_tree_from_index(filenames): 429 # Copy the environment, because the files have to be read from the original 430 # index. 431 env = os.environ.copy() 432 def index_contents_generator(): 433 for filename in filenames: 434 git_ls_files_cmd = ['git', 'ls-files', '--stage', '-z', '--', filename] 435 git_ls_files = subprocess.Popen(git_ls_files_cmd, env=env, 436 stdin=subprocess.PIPE, 437 stdout=subprocess.PIPE) 438 stdout = git_ls_files.communicate()[0] 439 yield convert_string(stdout.split(b'\0')[0]) 440 return create_tree(index_contents_generator(), '--index-info') 441 442 443def run_clang_format_and_save_to_tree(changed_lines, revision=None, 444 binary='clang-format', style=None): 445 """Run clang-format on each file and save the result to a git tree. 446 447 Returns the object ID (SHA-1) of the created tree.""" 448 # Copy the environment when formatting the files in the index, because the 449 # files have to be read from the original index. 450 env = os.environ.copy() if revision == '' else None 451 def iteritems(container): 452 try: 453 return container.iteritems() # Python 2 454 except AttributeError: 455 return container.items() # Python 3 456 def index_info_generator(): 457 for filename, line_ranges in iteritems(changed_lines): 458 if revision is not None: 459 if len(revision) > 0: 460 git_metadata_cmd = ['git', 'ls-tree', 461 '%s:%s' % (revision, os.path.dirname(filename)), 462 os.path.basename(filename)] 463 else: 464 git_metadata_cmd = ['git', 'ls-files', '--stage', '--', filename] 465 git_metadata = subprocess.Popen(git_metadata_cmd, env=env, 466 stdin=subprocess.PIPE, 467 stdout=subprocess.PIPE) 468 stdout = git_metadata.communicate()[0] 469 mode = oct(int(stdout.split()[0], 8)) 470 else: 471 mode = oct(os.stat(filename).st_mode) 472 # Adjust python3 octal format so that it matches what git expects 473 if mode.startswith('0o'): 474 mode = '0' + mode[2:] 475 blob_id = clang_format_to_blob(filename, line_ranges, 476 revision=revision, 477 binary=binary, 478 style=style, 479 env=env) 480 yield '%s %s\t%s' % (mode, blob_id, filename) 481 return create_tree(index_info_generator(), '--index-info') 482 483 484def create_tree(input_lines, mode): 485 """Create a tree object from the given input. 486 487 If mode is '--stdin', it must be a list of filenames. If mode is 488 '--index-info' is must be a list of values suitable for "git update-index 489 --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode 490 is invalid.""" 491 assert mode in ('--stdin', '--index-info') 492 cmd = ['git', 'update-index', '--add', '-z', mode] 493 with temporary_index_file(): 494 p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 495 for line in input_lines: 496 p.stdin.write(to_bytes('%s\0' % line)) 497 p.stdin.close() 498 if p.wait() != 0: 499 die('`%s` failed' % ' '.join(cmd)) 500 tree_id = run('git', 'write-tree') 501 return tree_id 502 503 504def clang_format_to_blob(filename, line_ranges, revision=None, 505 binary='clang-format', style=None, env=None): 506 """Run clang-format on the given file and save the result to a git blob. 507 508 Runs on the file in `revision` if not None, or on the file in the working 509 directory if `revision` is None. Revision can be set to an empty string to run 510 clang-format on the file in the index. 511 512 Returns the object ID (SHA-1) of the created blob.""" 513 clang_format_cmd = [binary] 514 if style: 515 clang_format_cmd.extend(['--style='+style]) 516 clang_format_cmd.extend([ 517 '--lines=%s:%s' % (start_line, start_line+line_count-1) 518 for start_line, line_count in line_ranges]) 519 if revision is not None: 520 clang_format_cmd.extend(['--assume-filename='+filename]) 521 git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)] 522 git_show = subprocess.Popen(git_show_cmd, env=env, stdin=subprocess.PIPE, 523 stdout=subprocess.PIPE) 524 git_show.stdin.close() 525 clang_format_stdin = git_show.stdout 526 else: 527 clang_format_cmd.extend([filename]) 528 git_show = None 529 clang_format_stdin = subprocess.PIPE 530 try: 531 clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, 532 stdout=subprocess.PIPE) 533 if clang_format_stdin == subprocess.PIPE: 534 clang_format_stdin = clang_format.stdin 535 except OSError as e: 536 if e.errno == errno.ENOENT: 537 die('cannot find executable "%s"' % binary) 538 else: 539 raise 540 clang_format_stdin.close() 541 hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 542 hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 543 stdout=subprocess.PIPE) 544 clang_format.stdout.close() 545 stdout = hash_object.communicate()[0] 546 if hash_object.returncode != 0: 547 die('`%s` failed' % ' '.join(hash_object_cmd)) 548 if clang_format.wait() != 0: 549 die('`%s` failed' % ' '.join(clang_format_cmd)) 550 if git_show and git_show.wait() != 0: 551 die('`%s` failed' % ' '.join(git_show_cmd)) 552 return convert_string(stdout).rstrip('\r\n') 553 554 555@contextlib.contextmanager 556def temporary_index_file(tree=None): 557 """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 558 the file afterward.""" 559 index_path = create_temporary_index(tree) 560 old_index_path = os.environ.get('GIT_INDEX_FILE') 561 os.environ['GIT_INDEX_FILE'] = index_path 562 try: 563 yield 564 finally: 565 if old_index_path is None: 566 del os.environ['GIT_INDEX_FILE'] 567 else: 568 os.environ['GIT_INDEX_FILE'] = old_index_path 569 os.remove(index_path) 570 571 572def create_temporary_index(tree=None): 573 """Create a temporary index file and return the created file's path. 574 575 If `tree` is not None, use that as the tree to read in. Otherwise, an 576 empty index is created.""" 577 gitdir = run('git', 'rev-parse', '--git-dir') 578 path = os.path.join(gitdir, temp_index_basename) 579 if tree is None: 580 tree = '--empty' 581 run('git', 'read-tree', '--index-output='+path, tree) 582 return path 583 584 585def print_diff(old_tree, new_tree): 586 """Print the diff between the two trees to stdout.""" 587 # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 588 # is expected to be viewed by the user, and only the former does nice things 589 # like color and pagination. 590 # 591 # We also only print modified files since `new_tree` only contains the files 592 # that were modified, so unmodified files would show as deleted without the 593 # filter. 594 return subprocess.run(['git', 'diff', '--diff-filter=M', 595 '--exit-code', old_tree, new_tree]).returncode 596 597def print_diffstat(old_tree, new_tree): 598 """Print the diffstat between the two trees to stdout.""" 599 # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 600 # is expected to be viewed by the user, and only the former does nice things 601 # like color and pagination. 602 # 603 # We also only print modified files since `new_tree` only contains the files 604 # that were modified, so unmodified files would show as deleted without the 605 # filter. 606 return subprocess.run(['git', 'diff', '--diff-filter=M', '--exit-code', 607 '--stat', old_tree, new_tree]).returncode 608 609def apply_changes(old_tree, new_tree, force=False, patch_mode=False): 610 """Apply the changes in `new_tree` to the working directory. 611 612 Bails if there are local changes in those files and not `force`. If 613 `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 614 changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z', 615 '--name-only', old_tree, 616 new_tree).rstrip('\0').split('\0') 617 if not force: 618 unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 619 if unstaged_files: 620 print('The following files would be modified but ' 621 'have unstaged changes:', file=sys.stderr) 622 print(unstaged_files, file=sys.stderr) 623 print('Please commit, stage, or stash them first.', file=sys.stderr) 624 sys.exit(2) 625 if patch_mode: 626 # In patch mode, we could just as well create an index from the new tree 627 # and checkout from that, but then the user will be presented with a 628 # message saying "Discard ... from worktree". Instead, we use the old 629 # tree as the index and checkout from new_tree, which gives the slightly 630 # better message, "Apply ... to index and worktree". This is not quite 631 # right, since it won't be applied to the user's index, but oh well. 632 with temporary_index_file(old_tree): 633 subprocess.run(['git', 'checkout', '--patch', new_tree], check=True) 634 index_tree = old_tree 635 else: 636 with temporary_index_file(new_tree): 637 run('git', 'checkout-index', '-f', '--', *changed_files) 638 return changed_files 639 640 641def run(*args, **kwargs): 642 stdin = kwargs.pop('stdin', '') 643 verbose = kwargs.pop('verbose', True) 644 strip = kwargs.pop('strip', True) 645 for name in kwargs: 646 raise TypeError("run() got an unexpected keyword argument '%s'" % name) 647 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 648 stdin=subprocess.PIPE) 649 stdout, stderr = p.communicate(input=stdin) 650 651 stdout = convert_string(stdout) 652 stderr = convert_string(stderr) 653 654 if p.returncode == 0: 655 if stderr: 656 if verbose: 657 print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr) 658 print(stderr.rstrip(), file=sys.stderr) 659 if strip: 660 stdout = stdout.rstrip('\r\n') 661 return stdout 662 if verbose: 663 print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr) 664 if stderr: 665 print(stderr.rstrip(), file=sys.stderr) 666 sys.exit(2) 667 668 669def die(message): 670 print('error:', message, file=sys.stderr) 671 sys.exit(2) 672 673 674def to_bytes(str_input): 675 # Encode to UTF-8 to get binary data. 676 if isinstance(str_input, bytes): 677 return str_input 678 return str_input.encode('utf-8') 679 680 681def to_string(bytes_input): 682 if isinstance(bytes_input, str): 683 return bytes_input 684 return bytes_input.encode('utf-8') 685 686 687def convert_string(bytes_input): 688 try: 689 return to_string(bytes_input.decode('utf-8')) 690 except AttributeError: # 'str' object has no attribute 'decode'. 691 return str(bytes_input) 692 except UnicodeError: 693 return str(bytes_input) 694 695if __name__ == '__main__': 696 sys.exit(main()) 697