proxygen
shell_quoting.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # Copyright (c) Facebook, Inc. and its affiliates.
3 from __future__ import absolute_import
4 from __future__ import division
5 from __future__ import print_function
6 from __future__ import unicode_literals
7 '''
8 
9 Almost every FBCodeBuilder string is ultimately passed to a shell. Escaping
10 too little or too much tends to be the most common error. The utilities in
11 this file give a systematic way of avoiding such bugs:
12  - When you write literal strings destined for the shell, use `ShellQuoted`.
13  - When these literal strings are parameterized, use `ShellQuoted.format`.
14  - Any parameters that are raw strings get `shell_quote`d automatically,
15  while any ShellQuoted parameters will be left intact.
16  - Use `path_join` to join path components.
17  - Use `shell_join` to join already-quoted command arguments or shell lines.
18 
19 '''
20 
21 import os
22 
23 from collections import namedtuple
24 
25 
26 class ShellQuoted(namedtuple('ShellQuoted', ('do_not_use_raw_str',))):
27  '''
28 
29  Wrap a string with this to make it transparent to shell_quote(). It
30  will almost always suffice to use ShellQuoted.format(), path_join(),
31  or shell_join().
32 
33  If you really must, use raw_shell() to access the raw string.
34 
35  '''
36 
37  def __new__(cls, s):
38  'No need to nest ShellQuoted.'
39  return super(ShellQuoted, cls).__new__(
40  cls, s.do_not_use_raw_str if isinstance(s, ShellQuoted) else s
41  )
42 
43  def __str__(self):
44  raise RuntimeError(
45  'One does not simply convert {0} to a string -- use path_join() '
46  'or ShellQuoted.format() instead'.format(repr(self))
47  )
48 
49  def __repr__(self):
50  return '{0}({1})'.format(
51  self.__class__.__name__, repr(self.do_not_use_raw_str)
52  )
53 
54  def format(self, **kwargs):
55  '''
56 
57  Use instead of str.format() when the arguments are either
58  `ShellQuoted()` or raw strings needing to be `shell_quote()`d.
59 
60  Positional args are deliberately not supported since they are more
61  error-prone.
62 
63  '''
64  return ShellQuoted(self.do_not_use_raw_str.format(**dict(
65  (k, shell_quote(v).do_not_use_raw_str) for k, v in kwargs.items()
66  )))
67 
68 
69 def shell_quote(s):
70  'Quotes a string if it is not already quoted'
71  return s if isinstance(s, ShellQuoted) \
72  else ShellQuoted("'" + str(s).replace("'", "'\\''") + "'")
73 
74 
75 def raw_shell(s):
76  'Not a member of ShellQuoted so we get a useful error for raw strings'
77  if isinstance(s, ShellQuoted):
78  return s.do_not_use_raw_str
79  raise RuntimeError('{0} should have been ShellQuoted'.format(s))
80 
81 
82 def shell_join(delim, it):
83  'Joins an iterable of ShellQuoted with a delimiter between each two'
84  return ShellQuoted(delim.join(raw_shell(s) for s in it))
85 
86 
87 def path_join(*args):
88  'Joins ShellQuoted and raw pieces of paths to make a shell-quoted path'
89  return ShellQuoted(os.path.join(*[
90  raw_shell(shell_quote(s)) for s in args
91  ]))
92 
93 
94 def shell_comment(c):
95  'Do not shell-escape raw strings in comments, but do handle line breaks.'
96  return ShellQuoted('# {c}').format(c=ShellQuoted(
97  (raw_shell(c) if isinstance(c, ShellQuoted) else c)
98  .replace('\n', '\n# ')
99  ))
def raw_shell(s)
def shell_quote(s)
void BENCHFUN() replace(size_t iters, size_t arg)
def shell_comment(c)
def path_join(args)
def format(self, kwargs)
def shell_join(delim, it)