xref: /llvm-project/clang/tools/clang-format/git-clang-format (revision 56ab56c85729976f29d5de2fd73912449cb6da7c)
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