portageq: portage.util._argparse
[portage.git] / bin / chpathtool.py
1 #!/usr/bin/python
2 # Copyright 2011-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 import io
6 import optparse
7 import os
8 import stat
9 import sys
10
11 CONTENT_ENCODING = "utf_8"
12 FS_ENCODING = "utf_8"
13
14 try:
15         import magic
16 except ImportError:
17         magic = None
18 else:
19         try:
20                 magic.MIME_TYPE
21         except AttributeError:
22                 # magic module seems to be broken
23                 magic = None
24
25 class IsTextFile(object):
26
27         def __init__(self):
28                 if magic is not None:
29                         self._call = self._is_text_magic
30                         self._m = magic.open(magic.MIME_TYPE)
31                         self._m.load()
32                 else:
33                         self._call = self._is_text_encoding
34                         self._encoding = CONTENT_ENCODING
35
36         def __call__(self, filename):
37                 """
38                 Returns True if the given file is a text file, and False otherwise.
39                 """
40                 return self._call(filename)
41
42         def _is_text_magic(self, filename):
43                 mime_type = self._m.file(filename)
44                 if isinstance(mime_type, bytes):
45                         mime_type = mime_type.decode('ascii', 'replace')
46                 return mime_type.startswith("text/")
47
48         def _is_text_encoding(self, filename):
49                 try:
50                         for line in io.open(filename, mode='r', encoding=self._encoding):
51                                 pass
52                 except UnicodeDecodeError:
53                         return False
54                 return True
55
56 def chpath_inplace(filename, is_text_file, old, new):
57         """
58         Returns True if any modifications were made, and False otherwise.
59         """
60
61         modified = False
62         orig_stat = os.lstat(filename)
63         try:
64                 f = io.open(filename, buffering=0, mode='r+b')
65         except IOError:
66                 try:
67                         orig_mode = stat.S_IMODE(os.lstat(filename).st_mode)
68                 except OSError as e:
69                         sys.stderr.write("%s: %s\n" % (e, filename))
70                         return
71                 temp_mode = 0o200 | orig_mode
72                 os.chmod(filename, temp_mode)
73                 try:
74                         f = io.open(filename, buffering=0, mode='r+b')
75                 finally:
76                         os.chmod(filename, orig_mode)
77
78         len_old = len(old)
79         len_new = len(new)
80         matched_byte_count = 0
81         while True:
82                 in_byte = f.read(1)
83
84                 if not in_byte:
85                         break
86
87                 if in_byte == old[matched_byte_count]:
88                         matched_byte_count += 1
89                         if matched_byte_count == len_old:
90                                 modified = True
91                                 matched_byte_count = 0
92                                 end_position = f.tell()
93                                 start_position = end_position - len_old
94                                 if not is_text_file:
95                                         # search backwards for leading slashes written by
96                                         # a previous invocation of this tool
97                                         num_to_write = len_old
98                                         f.seek(start_position - 1)
99                                         while True:
100                                                 if f.read(1) != b'/':
101                                                         break
102                                                 num_to_write += 1
103                                                 f.seek(f.tell() - 2)
104
105                                         # pad with as many leading slashes as necessary
106                                         while num_to_write > len_new:
107                                                 f.write(b'/')
108                                                 num_to_write -= 1
109                                         f.write(new)
110                                 else:
111                                         remainder = f.read()
112                                         f.seek(start_position)
113                                         f.write(new)
114                                         if remainder:
115                                                 f.write(remainder)
116                                                 f.truncate()
117                                                 f.seek(start_position + len_new)
118                 elif matched_byte_count > 0:
119                         # back up an try to start a new match after
120                         # the first byte of the previous partial match
121                         f.seek(f.tell() - matched_byte_count)
122                         matched_byte_count = 0
123
124         f.close()
125         if modified:
126                 if sys.hexversion >= 0x3030000:
127                         orig_mtime = orig_stat.st_mtime_ns
128                         os.utime(filename, ns=(orig_mtime, orig_mtime))
129                 else:
130                         orig_mtime = orig_stat[stat.ST_MTIME]
131                         os.utime(filename, (orig_mtime, orig_mtime))
132         return modified
133
134 def chpath_inplace_symlink(filename, st, old, new):
135         target = os.readlink(filename)
136         if target.startswith(old):
137                 new_target = new + target[len(old):]
138                 os.unlink(filename)
139                 os.symlink(new_target, filename)
140                 os.lchown(filename, st.st_uid, st.st_gid)
141
142 def main(argv):
143
144         usage = "%s [options] <location> <old> <new>" % (os.path.basename(argv[0],))
145         parser = optparse.OptionParser(usage=usage)
146         options, args = parser.parse_args(argv[1:])
147
148         if len(args) != 3:
149                 parser.error("3 args required, got %s" % (len(args),))
150
151         location, old, new = args
152
153         is_text_file = IsTextFile()
154
155         if not isinstance(location, bytes):
156                 location = location.encode(FS_ENCODING)
157         if not isinstance(old, bytes):
158                 old = old.encode(FS_ENCODING)
159         if not isinstance(new, bytes):
160                 new = new.encode(FS_ENCODING)
161
162         st = os.lstat(location)
163
164         if stat.S_ISDIR(st.st_mode):
165                 for parent, dirs, files in os.walk(location):
166                         for filename in files:
167                                 filename = os.path.join(parent, filename)
168                                 try:
169                                         st = os.lstat(filename)
170                                 except OSError:
171                                         pass
172                                 else:
173                                         if stat.S_ISREG(st.st_mode):
174                                                 chpath_inplace(filename,
175                                                         is_text_file(filename), old, new)
176                                         elif stat.S_ISLNK(st.st_mode):
177                                                 chpath_inplace_symlink(filename, st, old, new)
178
179         elif stat.S_ISREG(st.st_mode):
180                 chpath_inplace(location,
181                         is_text_file(location), old, new)
182         elif stat.S_ISLNK(st.st_mode):
183                 chpath_inplace_symlink(location, st, old, new)
184
185         return os.EX_OK
186
187 if __name__ == "__main__":
188         sys.exit(main(sys.argv))