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