##// END OF EJS Templates
typing: add trivial type hints to rest of the windows platform module...
Matt Harbison -
r50712:2b147671 default
parent child Browse files
Show More
@@ -1,746 +1,750 b''
1 # windows.py - Windows utility function implementations for Mercurial
1 # windows.py - Windows utility function implementations for Mercurial
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import errno
9 import errno
10 import getpass
10 import getpass
11 import msvcrt # pytype: disable=import-error
11 import msvcrt # pytype: disable=import-error
12 import os
12 import os
13 import re
13 import re
14 import stat
14 import stat
15 import string
15 import string
16 import sys
16 import sys
17 import winreg # pytype: disable=import-error
17 import winreg # pytype: disable=import-error
18
18
19 from typing import (
19 from typing import (
20 AnyStr,
20 BinaryIO,
21 BinaryIO,
21 Iterable,
22 Iterable,
22 Iterator,
23 Iterator,
23 List,
24 List,
25 Mapping,
24 NoReturn,
26 NoReturn,
25 Optional,
27 Optional,
28 Pattern,
26 Sequence,
29 Sequence,
27 Union,
30 Union,
28 )
31 )
29
32
30 from .i18n import _
33 from .i18n import _
31 from .pycompat import getattr
34 from .pycompat import getattr
32 from . import (
35 from . import (
33 encoding,
36 encoding,
34 error,
37 error,
35 policy,
38 policy,
36 pycompat,
39 pycompat,
37 typelib,
40 typelib,
38 win32,
41 win32,
39 )
42 )
40
43
41
44
42 osutil = policy.importmod('osutil')
45 osutil = policy.importmod('osutil')
43
46
44 getfsmountpoint = win32.getvolumename
47 getfsmountpoint = win32.getvolumename
45 getfstype = win32.getfstype
48 getfstype = win32.getfstype
46 getuser = win32.getuser
49 getuser = win32.getuser
47 hidewindow = win32.hidewindow
50 hidewindow = win32.hidewindow
48 makedir = win32.makedir
51 makedir = win32.makedir
49 nlinks = win32.nlinks
52 nlinks = win32.nlinks
50 oslink = win32.oslink
53 oslink = win32.oslink
51 samedevice = win32.samedevice
54 samedevice = win32.samedevice
52 samefile = win32.samefile
55 samefile = win32.samefile
53 setsignalhandler = win32.setsignalhandler
56 setsignalhandler = win32.setsignalhandler
54 spawndetached = win32.spawndetached
57 spawndetached = win32.spawndetached
55 split = os.path.split
58 split = os.path.split
56 testpid = win32.testpid
59 testpid = win32.testpid
57 unlink = win32.unlink
60 unlink = win32.unlink
58
61
59 umask = 0o022
62 umask: int = 0o022
60
63
61
64
62 class mixedfilemodewrapper:
65 class mixedfilemodewrapper:
63 """Wraps a file handle when it is opened in read/write mode.
66 """Wraps a file handle when it is opened in read/write mode.
64
67
65 fopen() and fdopen() on Windows have a specific-to-Windows requirement
68 fopen() and fdopen() on Windows have a specific-to-Windows requirement
66 that files opened with mode r+, w+, or a+ make a call to a file positioning
69 that files opened with mode r+, w+, or a+ make a call to a file positioning
67 function when switching between reads and writes. Without this extra call,
70 function when switching between reads and writes. Without this extra call,
68 Python will raise a not very intuitive "IOError: [Errno 0] Error."
71 Python will raise a not very intuitive "IOError: [Errno 0] Error."
69
72
70 This class wraps posixfile instances when the file is opened in read/write
73 This class wraps posixfile instances when the file is opened in read/write
71 mode and automatically adds checks or inserts appropriate file positioning
74 mode and automatically adds checks or inserts appropriate file positioning
72 calls when necessary.
75 calls when necessary.
73 """
76 """
74
77
75 OPNONE = 0
78 OPNONE = 0
76 OPREAD = 1
79 OPREAD = 1
77 OPWRITE = 2
80 OPWRITE = 2
78
81
79 def __init__(self, fp):
82 def __init__(self, fp):
80 object.__setattr__(self, '_fp', fp)
83 object.__setattr__(self, '_fp', fp)
81 object.__setattr__(self, '_lastop', 0)
84 object.__setattr__(self, '_lastop', 0)
82
85
83 def __enter__(self):
86 def __enter__(self):
84 self._fp.__enter__()
87 self._fp.__enter__()
85 return self
88 return self
86
89
87 def __exit__(self, exc_type, exc_val, exc_tb):
90 def __exit__(self, exc_type, exc_val, exc_tb):
88 self._fp.__exit__(exc_type, exc_val, exc_tb)
91 self._fp.__exit__(exc_type, exc_val, exc_tb)
89
92
90 def __getattr__(self, name):
93 def __getattr__(self, name):
91 return getattr(self._fp, name)
94 return getattr(self._fp, name)
92
95
93 def __setattr__(self, name, value):
96 def __setattr__(self, name, value):
94 return self._fp.__setattr__(name, value)
97 return self._fp.__setattr__(name, value)
95
98
96 def _noopseek(self):
99 def _noopseek(self):
97 self._fp.seek(0, os.SEEK_CUR)
100 self._fp.seek(0, os.SEEK_CUR)
98
101
99 def seek(self, *args, **kwargs):
102 def seek(self, *args, **kwargs):
100 object.__setattr__(self, '_lastop', self.OPNONE)
103 object.__setattr__(self, '_lastop', self.OPNONE)
101 return self._fp.seek(*args, **kwargs)
104 return self._fp.seek(*args, **kwargs)
102
105
103 def write(self, d):
106 def write(self, d):
104 if self._lastop == self.OPREAD:
107 if self._lastop == self.OPREAD:
105 self._noopseek()
108 self._noopseek()
106
109
107 object.__setattr__(self, '_lastop', self.OPWRITE)
110 object.__setattr__(self, '_lastop', self.OPWRITE)
108 return self._fp.write(d)
111 return self._fp.write(d)
109
112
110 def writelines(self, *args, **kwargs):
113 def writelines(self, *args, **kwargs):
111 if self._lastop == self.OPREAD:
114 if self._lastop == self.OPREAD:
112 self._noopeseek()
115 self._noopeseek()
113
116
114 object.__setattr__(self, '_lastop', self.OPWRITE)
117 object.__setattr__(self, '_lastop', self.OPWRITE)
115 return self._fp.writelines(*args, **kwargs)
118 return self._fp.writelines(*args, **kwargs)
116
119
117 def read(self, *args, **kwargs):
120 def read(self, *args, **kwargs):
118 if self._lastop == self.OPWRITE:
121 if self._lastop == self.OPWRITE:
119 self._noopseek()
122 self._noopseek()
120
123
121 object.__setattr__(self, '_lastop', self.OPREAD)
124 object.__setattr__(self, '_lastop', self.OPREAD)
122 return self._fp.read(*args, **kwargs)
125 return self._fp.read(*args, **kwargs)
123
126
124 def readline(self, *args, **kwargs):
127 def readline(self, *args, **kwargs):
125 if self._lastop == self.OPWRITE:
128 if self._lastop == self.OPWRITE:
126 self._noopseek()
129 self._noopseek()
127
130
128 object.__setattr__(self, '_lastop', self.OPREAD)
131 object.__setattr__(self, '_lastop', self.OPREAD)
129 return self._fp.readline(*args, **kwargs)
132 return self._fp.readline(*args, **kwargs)
130
133
131 def readlines(self, *args, **kwargs):
134 def readlines(self, *args, **kwargs):
132 if self._lastop == self.OPWRITE:
135 if self._lastop == self.OPWRITE:
133 self._noopseek()
136 self._noopseek()
134
137
135 object.__setattr__(self, '_lastop', self.OPREAD)
138 object.__setattr__(self, '_lastop', self.OPREAD)
136 return self._fp.readlines(*args, **kwargs)
139 return self._fp.readlines(*args, **kwargs)
137
140
138
141
139 class fdproxy:
142 class fdproxy:
140 """Wraps osutil.posixfile() to override the name attribute to reflect the
143 """Wraps osutil.posixfile() to override the name attribute to reflect the
141 underlying file name.
144 underlying file name.
142 """
145 """
143
146
144 def __init__(self, name, fp):
147 def __init__(self, name, fp):
145 self.name = name
148 self.name = name
146 self._fp = fp
149 self._fp = fp
147
150
148 def __enter__(self):
151 def __enter__(self):
149 self._fp.__enter__()
152 self._fp.__enter__()
150 # Return this wrapper for the context manager so that the name is
153 # Return this wrapper for the context manager so that the name is
151 # still available.
154 # still available.
152 return self
155 return self
153
156
154 def __exit__(self, exc_type, exc_value, traceback):
157 def __exit__(self, exc_type, exc_value, traceback):
155 self._fp.__exit__(exc_type, exc_value, traceback)
158 self._fp.__exit__(exc_type, exc_value, traceback)
156
159
157 def __iter__(self):
160 def __iter__(self):
158 return iter(self._fp)
161 return iter(self._fp)
159
162
160 def __getattr__(self, name):
163 def __getattr__(self, name):
161 return getattr(self._fp, name)
164 return getattr(self._fp, name)
162
165
163
166
164 def posixfile(name, mode=b'r', buffering=-1):
167 def posixfile(name, mode=b'r', buffering=-1):
165 '''Open a file with even more POSIX-like semantics'''
168 '''Open a file with even more POSIX-like semantics'''
166 try:
169 try:
167 fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError
170 fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError
168
171
169 # PyFile_FromFd() ignores the name, and seems to report fp.name as the
172 # PyFile_FromFd() ignores the name, and seems to report fp.name as the
170 # underlying file descriptor.
173 # underlying file descriptor.
171 fp = fdproxy(name, fp)
174 fp = fdproxy(name, fp)
172
175
173 # The position when opening in append mode is implementation defined, so
176 # The position when opening in append mode is implementation defined, so
174 # make it consistent with other platforms, which position at EOF.
177 # make it consistent with other platforms, which position at EOF.
175 if b'a' in mode:
178 if b'a' in mode:
176 fp.seek(0, os.SEEK_END)
179 fp.seek(0, os.SEEK_END)
177
180
178 if b'+' in mode:
181 if b'+' in mode:
179 return mixedfilemodewrapper(fp)
182 return mixedfilemodewrapper(fp)
180
183
181 return fp
184 return fp
182 except WindowsError as err: # pytype: disable=name-error
185 except WindowsError as err: # pytype: disable=name-error
183 # convert to a friendlier exception
186 # convert to a friendlier exception
184 raise IOError(
187 raise IOError(
185 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
188 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
186 )
189 )
187
190
188
191
189 # may be wrapped by win32mbcs extension
192 # may be wrapped by win32mbcs extension
190 listdir = osutil.listdir
193 listdir = osutil.listdir
191
194
192
195
193 def get_password() -> bytes:
196 def get_password() -> bytes:
194 """Prompt for password with echo off, using Windows getch().
197 """Prompt for password with echo off, using Windows getch().
195
198
196 This shouldn't be called directly- use ``ui.getpass()`` instead, which
199 This shouldn't be called directly- use ``ui.getpass()`` instead, which
197 checks if the session is interactive first.
200 checks if the session is interactive first.
198 """
201 """
199 pw = u""
202 pw = u""
200 while True:
203 while True:
201 c = msvcrt.getwch() # pytype: disable=module-attr
204 c = msvcrt.getwch() # pytype: disable=module-attr
202 if c == u'\r' or c == u'\n':
205 if c == u'\r' or c == u'\n':
203 break
206 break
204 if c == u'\003':
207 if c == u'\003':
205 raise KeyboardInterrupt
208 raise KeyboardInterrupt
206 if c == u'\b':
209 if c == u'\b':
207 pw = pw[:-1]
210 pw = pw[:-1]
208 else:
211 else:
209 pw = pw + c
212 pw = pw + c
210 msvcrt.putwch(u'\r') # pytype: disable=module-attr
213 msvcrt.putwch(u'\r') # pytype: disable=module-attr
211 msvcrt.putwch(u'\n') # pytype: disable=module-attr
214 msvcrt.putwch(u'\n') # pytype: disable=module-attr
212 return encoding.unitolocal(pw)
215 return encoding.unitolocal(pw)
213
216
214
217
215 class winstdout(typelib.BinaryIO_Proxy):
218 class winstdout(typelib.BinaryIO_Proxy):
216 """Some files on Windows misbehave.
219 """Some files on Windows misbehave.
217
220
218 When writing to a broken pipe, EINVAL instead of EPIPE may be raised.
221 When writing to a broken pipe, EINVAL instead of EPIPE may be raised.
219
222
220 When writing too many bytes to a console at the same, a "Not enough space"
223 When writing too many bytes to a console at the same, a "Not enough space"
221 error may happen. Python 3 already works around that.
224 error may happen. Python 3 already works around that.
222 """
225 """
223
226
224 def __init__(self, fp: BinaryIO):
227 def __init__(self, fp: BinaryIO):
225 self.fp = fp
228 self.fp = fp
226
229
227 def __getattr__(self, key):
230 def __getattr__(self, key):
228 return getattr(self.fp, key)
231 return getattr(self.fp, key)
229
232
230 def close(self):
233 def close(self):
231 try:
234 try:
232 self.fp.close()
235 self.fp.close()
233 except IOError:
236 except IOError:
234 pass
237 pass
235
238
236 def write(self, s):
239 def write(self, s):
237 try:
240 try:
238 return self.fp.write(s)
241 return self.fp.write(s)
239 except IOError as inst:
242 except IOError as inst:
240 if inst.errno != 0 and not win32.lasterrorwaspipeerror(inst):
243 if inst.errno != 0 and not win32.lasterrorwaspipeerror(inst):
241 raise
244 raise
242 self.close()
245 self.close()
243 raise IOError(errno.EPIPE, 'Broken pipe')
246 raise IOError(errno.EPIPE, 'Broken pipe')
244
247
245 def flush(self):
248 def flush(self):
246 try:
249 try:
247 return self.fp.flush()
250 return self.fp.flush()
248 except IOError as inst:
251 except IOError as inst:
249 if not win32.lasterrorwaspipeerror(inst):
252 if not win32.lasterrorwaspipeerror(inst):
250 raise
253 raise
251 raise IOError(errno.EPIPE, 'Broken pipe')
254 raise IOError(errno.EPIPE, 'Broken pipe')
252
255
253
256
254 def openhardlinks() -> bool:
257 def openhardlinks() -> bool:
255 return True
258 return True
256
259
257
260
258 def parsepatchoutput(output_line: bytes) -> bytes:
261 def parsepatchoutput(output_line: bytes) -> bytes:
259 """parses the output produced by patch and returns the filename"""
262 """parses the output produced by patch and returns the filename"""
260 pf = output_line[14:]
263 pf = output_line[14:]
261 if pf[0] == b'`':
264 if pf[0] == b'`':
262 pf = pf[1:-1] # Remove the quotes
265 pf = pf[1:-1] # Remove the quotes
263 return pf
266 return pf
264
267
265
268
266 def sshargs(
269 def sshargs(
267 sshcmd: bytes, host: bytes, user: Optional[bytes], port: Optional[bytes]
270 sshcmd: bytes, host: bytes, user: Optional[bytes], port: Optional[bytes]
268 ) -> bytes:
271 ) -> bytes:
269 '''Build argument list for ssh or Plink'''
272 '''Build argument list for ssh or Plink'''
270 pflag = b'plink' in sshcmd.lower() and b'-P' or b'-p'
273 pflag = b'plink' in sshcmd.lower() and b'-P' or b'-p'
271 args = user and (b"%s@%s" % (user, host)) or host
274 args = user and (b"%s@%s" % (user, host)) or host
272 if args.startswith(b'-') or args.startswith(b'/'):
275 if args.startswith(b'-') or args.startswith(b'/'):
273 raise error.Abort(
276 raise error.Abort(
274 _(b'illegal ssh hostname or username starting with - or /: %s')
277 _(b'illegal ssh hostname or username starting with - or /: %s')
275 % args
278 % args
276 )
279 )
277 args = shellquote(args)
280 args = shellquote(args)
278 if port:
281 if port:
279 args = b'%s %s %s' % (pflag, shellquote(port), args)
282 args = b'%s %s %s' % (pflag, shellquote(port), args)
280 return args
283 return args
281
284
282
285
283 def setflags(f: bytes, l: bool, x: bool) -> None:
286 def setflags(f: bytes, l: bool, x: bool) -> None:
284 pass
287 pass
285
288
286
289
287 def copymode(
290 def copymode(
288 src: bytes,
291 src: bytes,
289 dst: bytes,
292 dst: bytes,
290 mode: Optional[bytes] = None,
293 mode: Optional[bytes] = None,
291 enforcewritable: bool = False,
294 enforcewritable: bool = False,
292 ) -> None:
295 ) -> None:
293 pass
296 pass
294
297
295
298
296 def checkexec(path: bytes) -> bool:
299 def checkexec(path: bytes) -> bool:
297 return False
300 return False
298
301
299
302
300 def checklink(path: bytes) -> bool:
303 def checklink(path: bytes) -> bool:
301 return False
304 return False
302
305
303
306
304 def setbinary(fd) -> None:
307 def setbinary(fd) -> None:
305 # When run without console, pipes may expose invalid
308 # When run without console, pipes may expose invalid
306 # fileno(), usually set to -1.
309 # fileno(), usually set to -1.
307 fno = getattr(fd, 'fileno', None)
310 fno = getattr(fd, 'fileno', None)
308 if fno is not None and fno() >= 0:
311 if fno is not None and fno() >= 0:
309 msvcrt.setmode(fno(), os.O_BINARY) # pytype: disable=module-attr
312 msvcrt.setmode(fno(), os.O_BINARY) # pytype: disable=module-attr
310
313
311
314
312 def pconvert(path: bytes) -> bytes:
315 def pconvert(path: bytes) -> bytes:
313 return path.replace(pycompat.ossep, b'/')
316 return path.replace(pycompat.ossep, b'/')
314
317
315
318
316 def localpath(path: bytes) -> bytes:
319 def localpath(path: bytes) -> bytes:
317 return path.replace(b'/', b'\\')
320 return path.replace(b'/', b'\\')
318
321
319
322
320 def normpath(path):
323 def normpath(path: bytes) -> bytes:
321 return pconvert(os.path.normpath(path))
324 return pconvert(os.path.normpath(path))
322
325
323
326
324 def normcase(path: bytes) -> bytes:
327 def normcase(path: bytes) -> bytes:
325 return encoding.upper(path) # NTFS compares via upper()
328 return encoding.upper(path) # NTFS compares via upper()
326
329
327
330
328 DRIVE_RE_B = re.compile(b'^[a-z]:')
331 DRIVE_RE_B: Pattern[bytes] = re.compile(b'^[a-z]:')
329 DRIVE_RE_S = re.compile('^[a-z]:')
332 DRIVE_RE_S: Pattern[str] = re.compile('^[a-z]:')
330
333
331
334
332 def abspath(path):
335 # TODO: why is this accepting str?
336 def abspath(path: AnyStr) -> AnyStr:
333 abs_path = os.path.abspath(path) # re-exports
337 abs_path = os.path.abspath(path) # re-exports
334 # Python on Windows is inconsistent regarding the capitalization of drive
338 # Python on Windows is inconsistent regarding the capitalization of drive
335 # letter and this cause issue with various path comparison along the way.
339 # letter and this cause issue with various path comparison along the way.
336 # So we normalize the drive later to upper case here.
340 # So we normalize the drive later to upper case here.
337 #
341 #
338 # See https://bugs.python.org/issue40368 for and example of this hell.
342 # See https://bugs.python.org/issue40368 for and example of this hell.
339 if isinstance(abs_path, bytes):
343 if isinstance(abs_path, bytes):
340 if DRIVE_RE_B.match(abs_path):
344 if DRIVE_RE_B.match(abs_path):
341 abs_path = abs_path[0:1].upper() + abs_path[1:]
345 abs_path = abs_path[0:1].upper() + abs_path[1:]
342 elif DRIVE_RE_S.match(abs_path):
346 elif DRIVE_RE_S.match(abs_path):
343 abs_path = abs_path[0:1].upper() + abs_path[1:]
347 abs_path = abs_path[0:1].upper() + abs_path[1:]
344 return abs_path
348 return abs_path
345
349
346
350
347 # see posix.py for definitions
351 # see posix.py for definitions
348 normcasespec = encoding.normcasespecs.upper
352 normcasespec: int = encoding.normcasespecs.upper
349 normcasefallback = encoding.upperfallback
353 normcasefallback = encoding.upperfallback
350
354
351
355
352 def samestat(s1, s2):
356 def samestat(s1: os.stat_result, s2: os.stat_result) -> bool:
353 return False
357 return False
354
358
355
359
356 def shelltocmdexe(path, env):
360 def shelltocmdexe(path: bytes, env: Mapping[bytes, bytes]) -> bytes:
357 r"""Convert shell variables in the form $var and ${var} inside ``path``
361 r"""Convert shell variables in the form $var and ${var} inside ``path``
358 to %var% form. Existing Windows style variables are left unchanged.
362 to %var% form. Existing Windows style variables are left unchanged.
359
363
360 The variables are limited to the given environment. Unknown variables are
364 The variables are limited to the given environment. Unknown variables are
361 left unchanged.
365 left unchanged.
362
366
363 >>> e = {b'var1': b'v1', b'var2': b'v2', b'var3': b'v3'}
367 >>> e = {b'var1': b'v1', b'var2': b'v2', b'var3': b'v3'}
364 >>> # Only valid values are expanded
368 >>> # Only valid values are expanded
365 >>> shelltocmdexe(b'cmd $var1 ${var2} %var3% $missing ${missing} %missing%',
369 >>> shelltocmdexe(b'cmd $var1 ${var2} %var3% $missing ${missing} %missing%',
366 ... e)
370 ... e)
367 'cmd %var1% %var2% %var3% $missing ${missing} %missing%'
371 'cmd %var1% %var2% %var3% $missing ${missing} %missing%'
368 >>> # Single quote prevents expansion, as does \$ escaping
372 >>> # Single quote prevents expansion, as does \$ escaping
369 >>> shelltocmdexe(b"cmd '$var1 ${var2} %var3%' \$var1 \${var2} \\", e)
373 >>> shelltocmdexe(b"cmd '$var1 ${var2} %var3%' \$var1 \${var2} \\", e)
370 'cmd "$var1 ${var2} %var3%" $var1 ${var2} \\'
374 'cmd "$var1 ${var2} %var3%" $var1 ${var2} \\'
371 >>> # $$ is not special. %% is not special either, but can be the end and
375 >>> # $$ is not special. %% is not special either, but can be the end and
372 >>> # start of consecutive variables
376 >>> # start of consecutive variables
373 >>> shelltocmdexe(b"cmd $$ %% %var1%%var2%", e)
377 >>> shelltocmdexe(b"cmd $$ %% %var1%%var2%", e)
374 'cmd $$ %% %var1%%var2%'
378 'cmd $$ %% %var1%%var2%'
375 >>> # No double substitution
379 >>> # No double substitution
376 >>> shelltocmdexe(b"$var1 %var1%", {b'var1': b'%var2%', b'var2': b'boom'})
380 >>> shelltocmdexe(b"$var1 %var1%", {b'var1': b'%var2%', b'var2': b'boom'})
377 '%var1% %var1%'
381 '%var1% %var1%'
378 >>> # Tilde expansion
382 >>> # Tilde expansion
379 >>> shelltocmdexe(b"~/dir ~\dir2 ~tmpfile \~/", {})
383 >>> shelltocmdexe(b"~/dir ~\dir2 ~tmpfile \~/", {})
380 '%USERPROFILE%/dir %USERPROFILE%\\dir2 ~tmpfile ~/'
384 '%USERPROFILE%/dir %USERPROFILE%\\dir2 ~tmpfile ~/'
381 """
385 """
382 if not any(c in path for c in b"$'~"):
386 if not any(c in path for c in b"$'~"):
383 return path
387 return path
384
388
385 varchars = pycompat.sysbytes(string.ascii_letters + string.digits) + b'_-'
389 varchars = pycompat.sysbytes(string.ascii_letters + string.digits) + b'_-'
386
390
387 res = b''
391 res = b''
388 index = 0
392 index = 0
389 pathlen = len(path)
393 pathlen = len(path)
390 while index < pathlen:
394 while index < pathlen:
391 c = path[index : index + 1]
395 c = path[index : index + 1]
392 if c == b'\'': # no expansion within single quotes
396 if c == b'\'': # no expansion within single quotes
393 path = path[index + 1 :]
397 path = path[index + 1 :]
394 pathlen = len(path)
398 pathlen = len(path)
395 try:
399 try:
396 index = path.index(b'\'')
400 index = path.index(b'\'')
397 res += b'"' + path[:index] + b'"'
401 res += b'"' + path[:index] + b'"'
398 except ValueError:
402 except ValueError:
399 res += c + path
403 res += c + path
400 index = pathlen - 1
404 index = pathlen - 1
401 elif c == b'%': # variable
405 elif c == b'%': # variable
402 path = path[index + 1 :]
406 path = path[index + 1 :]
403 pathlen = len(path)
407 pathlen = len(path)
404 try:
408 try:
405 index = path.index(b'%')
409 index = path.index(b'%')
406 except ValueError:
410 except ValueError:
407 res += b'%' + path
411 res += b'%' + path
408 index = pathlen - 1
412 index = pathlen - 1
409 else:
413 else:
410 var = path[:index]
414 var = path[:index]
411 res += b'%' + var + b'%'
415 res += b'%' + var + b'%'
412 elif c == b'$': # variable
416 elif c == b'$': # variable
413 if path[index + 1 : index + 2] == b'{':
417 if path[index + 1 : index + 2] == b'{':
414 path = path[index + 2 :]
418 path = path[index + 2 :]
415 pathlen = len(path)
419 pathlen = len(path)
416 try:
420 try:
417 index = path.index(b'}')
421 index = path.index(b'}')
418 var = path[:index]
422 var = path[:index]
419
423
420 # See below for why empty variables are handled specially
424 # See below for why empty variables are handled specially
421 if env.get(var, b'') != b'':
425 if env.get(var, b'') != b'':
422 res += b'%' + var + b'%'
426 res += b'%' + var + b'%'
423 else:
427 else:
424 res += b'${' + var + b'}'
428 res += b'${' + var + b'}'
425 except ValueError:
429 except ValueError:
426 res += b'${' + path
430 res += b'${' + path
427 index = pathlen - 1
431 index = pathlen - 1
428 else:
432 else:
429 var = b''
433 var = b''
430 index += 1
434 index += 1
431 c = path[index : index + 1]
435 c = path[index : index + 1]
432 while c != b'' and c in varchars:
436 while c != b'' and c in varchars:
433 var += c
437 var += c
434 index += 1
438 index += 1
435 c = path[index : index + 1]
439 c = path[index : index + 1]
436 # Some variables (like HG_OLDNODE) may be defined, but have an
440 # Some variables (like HG_OLDNODE) may be defined, but have an
437 # empty value. Those need to be skipped because when spawning
441 # empty value. Those need to be skipped because when spawning
438 # cmd.exe to run the hook, it doesn't replace %VAR% for an empty
442 # cmd.exe to run the hook, it doesn't replace %VAR% for an empty
439 # VAR, and that really confuses things like revset expressions.
443 # VAR, and that really confuses things like revset expressions.
440 # OTOH, if it's left in Unix format and the hook runs sh.exe, it
444 # OTOH, if it's left in Unix format and the hook runs sh.exe, it
441 # will substitute to an empty string, and everything is happy.
445 # will substitute to an empty string, and everything is happy.
442 if env.get(var, b'') != b'':
446 if env.get(var, b'') != b'':
443 res += b'%' + var + b'%'
447 res += b'%' + var + b'%'
444 else:
448 else:
445 res += b'$' + var
449 res += b'$' + var
446
450
447 if c != b'':
451 if c != b'':
448 index -= 1
452 index -= 1
449 elif (
453 elif (
450 c == b'~'
454 c == b'~'
451 and index + 1 < pathlen
455 and index + 1 < pathlen
452 and path[index + 1 : index + 2] in (b'\\', b'/')
456 and path[index + 1 : index + 2] in (b'\\', b'/')
453 ):
457 ):
454 res += b"%USERPROFILE%"
458 res += b"%USERPROFILE%"
455 elif (
459 elif (
456 c == b'\\'
460 c == b'\\'
457 and index + 1 < pathlen
461 and index + 1 < pathlen
458 and path[index + 1 : index + 2] in (b'$', b'~')
462 and path[index + 1 : index + 2] in (b'$', b'~')
459 ):
463 ):
460 # Skip '\', but only if it is escaping $ or ~
464 # Skip '\', but only if it is escaping $ or ~
461 res += path[index + 1 : index + 2]
465 res += path[index + 1 : index + 2]
462 index += 1
466 index += 1
463 else:
467 else:
464 res += c
468 res += c
465
469
466 index += 1
470 index += 1
467 return res
471 return res
468
472
469
473
470 # A sequence of backslashes is special iff it precedes a double quote:
474 # A sequence of backslashes is special iff it precedes a double quote:
471 # - if there's an even number of backslashes, the double quote is not
475 # - if there's an even number of backslashes, the double quote is not
472 # quoted (i.e. it ends the quoted region)
476 # quoted (i.e. it ends the quoted region)
473 # - if there's an odd number of backslashes, the double quote is quoted
477 # - if there's an odd number of backslashes, the double quote is quoted
474 # - in both cases, every pair of backslashes is unquoted into a single
478 # - in both cases, every pair of backslashes is unquoted into a single
475 # backslash
479 # backslash
476 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
480 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
477 # So, to quote a string, we must surround it in double quotes, double
481 # So, to quote a string, we must surround it in double quotes, double
478 # the number of backslashes that precede double quotes and add another
482 # the number of backslashes that precede double quotes and add another
479 # backslash before every double quote (being careful with the double
483 # backslash before every double quote (being careful with the double
480 # quote we've appended to the end)
484 # quote we've appended to the end)
481 _quotere = None
485 _quotere: Optional[Pattern[bytes]] = None
482 _needsshellquote = None
486 _needsshellquote = None
483
487
484
488
485 def shellquote(s: bytes) -> bytes:
489 def shellquote(s: bytes) -> bytes:
486 r"""
490 r"""
487 >>> shellquote(br'C:\Users\xyz')
491 >>> shellquote(br'C:\Users\xyz')
488 '"C:\\Users\\xyz"'
492 '"C:\\Users\\xyz"'
489 >>> shellquote(br'C:\Users\xyz/mixed')
493 >>> shellquote(br'C:\Users\xyz/mixed')
490 '"C:\\Users\\xyz/mixed"'
494 '"C:\\Users\\xyz/mixed"'
491 >>> # Would be safe not to quote too, since it is all double backslashes
495 >>> # Would be safe not to quote too, since it is all double backslashes
492 >>> shellquote(br'C:\\Users\\xyz')
496 >>> shellquote(br'C:\\Users\\xyz')
493 '"C:\\\\Users\\\\xyz"'
497 '"C:\\\\Users\\\\xyz"'
494 >>> # But this must be quoted
498 >>> # But this must be quoted
495 >>> shellquote(br'C:\\Users\\xyz/abc')
499 >>> shellquote(br'C:\\Users\\xyz/abc')
496 '"C:\\\\Users\\\\xyz/abc"'
500 '"C:\\\\Users\\\\xyz/abc"'
497 """
501 """
498 global _quotere
502 global _quotere
499 if _quotere is None:
503 if _quotere is None:
500 _quotere = re.compile(br'(\\*)("|\\$)')
504 _quotere = re.compile(br'(\\*)("|\\$)')
501 global _needsshellquote
505 global _needsshellquote
502 if _needsshellquote is None:
506 if _needsshellquote is None:
503 # ":" is also treated as "safe character", because it is used as a part
507 # ":" is also treated as "safe character", because it is used as a part
504 # of path name on Windows. "\" is also part of a path name, but isn't
508 # of path name on Windows. "\" is also part of a path name, but isn't
505 # safe because shlex.split() (kind of) treats it as an escape char and
509 # safe because shlex.split() (kind of) treats it as an escape char and
506 # drops it. It will leave the next character, even if it is another
510 # drops it. It will leave the next character, even if it is another
507 # "\".
511 # "\".
508 _needsshellquote = re.compile(br'[^a-zA-Z0-9._:/-]').search
512 _needsshellquote = re.compile(br'[^a-zA-Z0-9._:/-]').search
509 if s and not _needsshellquote(s) and not _quotere.search(s):
513 if s and not _needsshellquote(s) and not _quotere.search(s):
510 # "s" shouldn't have to be quoted
514 # "s" shouldn't have to be quoted
511 return s
515 return s
512 return b'"%s"' % _quotere.sub(br'\1\1\\\2', s)
516 return b'"%s"' % _quotere.sub(br'\1\1\\\2', s)
513
517
514
518
515 def _unquote(s):
519 def _unquote(s: bytes) -> bytes:
516 if s.startswith(b'"') and s.endswith(b'"'):
520 if s.startswith(b'"') and s.endswith(b'"'):
517 return s[1:-1]
521 return s[1:-1]
518 return s
522 return s
519
523
520
524
521 def shellsplit(s: bytes) -> List[bytes]:
525 def shellsplit(s: bytes) -> List[bytes]:
522 """Parse a command string in cmd.exe way (best-effort)"""
526 """Parse a command string in cmd.exe way (best-effort)"""
523 return pycompat.maplist(_unquote, pycompat.shlexsplit(s, posix=False))
527 return pycompat.maplist(_unquote, pycompat.shlexsplit(s, posix=False))
524
528
525
529
526 # if you change this stub into a real check, please try to implement the
530 # if you change this stub into a real check, please try to implement the
527 # username and groupname functions above, too.
531 # username and groupname functions above, too.
528 def isowner(st: os.stat_result) -> bool:
532 def isowner(st: os.stat_result) -> bool:
529 return True
533 return True
530
534
531
535
532 def findexe(command: bytes) -> Optional[bytes]:
536 def findexe(command: bytes) -> Optional[bytes]:
533 """Find executable for command searching like cmd.exe does.
537 """Find executable for command searching like cmd.exe does.
534 If command is a basename then PATH is searched for command.
538 If command is a basename then PATH is searched for command.
535 PATH isn't searched if command is an absolute or relative path.
539 PATH isn't searched if command is an absolute or relative path.
536 An extension from PATHEXT is found and added if not present.
540 An extension from PATHEXT is found and added if not present.
537 If command isn't found None is returned."""
541 If command isn't found None is returned."""
538 pathext = encoding.environ.get(b'PATHEXT', b'.COM;.EXE;.BAT;.CMD')
542 pathext = encoding.environ.get(b'PATHEXT', b'.COM;.EXE;.BAT;.CMD')
539 pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)]
543 pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)]
540 if os.path.splitext(command)[1].lower() in pathexts:
544 if os.path.splitext(command)[1].lower() in pathexts:
541 pathexts = [b'']
545 pathexts = [b'']
542
546
543 def findexisting(pathcommand: bytes) -> Optional[bytes]:
547 def findexisting(pathcommand: bytes) -> Optional[bytes]:
544 """Will append extension (if needed) and return existing file"""
548 """Will append extension (if needed) and return existing file"""
545 for ext in pathexts:
549 for ext in pathexts:
546 executable = pathcommand + ext
550 executable = pathcommand + ext
547 if os.path.exists(executable):
551 if os.path.exists(executable):
548 return executable
552 return executable
549 return None
553 return None
550
554
551 if pycompat.ossep in command:
555 if pycompat.ossep in command:
552 return findexisting(command)
556 return findexisting(command)
553
557
554 for path in encoding.environ.get(b'PATH', b'').split(pycompat.ospathsep):
558 for path in encoding.environ.get(b'PATH', b'').split(pycompat.ospathsep):
555 executable = findexisting(os.path.join(path, command))
559 executable = findexisting(os.path.join(path, command))
556 if executable is not None:
560 if executable is not None:
557 return executable
561 return executable
558 return findexisting(os.path.expanduser(os.path.expandvars(command)))
562 return findexisting(os.path.expanduser(os.path.expandvars(command)))
559
563
560
564
561 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
565 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
562
566
563
567
564 def statfiles(files: Sequence[bytes]) -> Iterator[Optional[os.stat_result]]:
568 def statfiles(files: Sequence[bytes]) -> Iterator[Optional[os.stat_result]]:
565 """Stat each file in files. Yield each stat, or None if a file
569 """Stat each file in files. Yield each stat, or None if a file
566 does not exist or has a type we don't care about.
570 does not exist or has a type we don't care about.
567
571
568 Cluster and cache stat per directory to minimize number of OS stat calls."""
572 Cluster and cache stat per directory to minimize number of OS stat calls."""
569 dircache = {} # dirname -> filename -> status | None if file does not exist
573 dircache = {} # dirname -> filename -> status | None if file does not exist
570 getkind = stat.S_IFMT
574 getkind = stat.S_IFMT
571 for nf in files:
575 for nf in files:
572 nf = normcase(nf)
576 nf = normcase(nf)
573 dir, base = os.path.split(nf)
577 dir, base = os.path.split(nf)
574 if not dir:
578 if not dir:
575 dir = b'.'
579 dir = b'.'
576 cache = dircache.get(dir, None)
580 cache = dircache.get(dir, None)
577 if cache is None:
581 if cache is None:
578 try:
582 try:
579 dmap = {
583 dmap = {
580 normcase(n): s
584 normcase(n): s
581 for n, k, s in listdir(dir, True)
585 for n, k, s in listdir(dir, True)
582 if getkind(s.st_mode) in _wantedkinds
586 if getkind(s.st_mode) in _wantedkinds
583 }
587 }
584 except (FileNotFoundError, NotADirectoryError):
588 except (FileNotFoundError, NotADirectoryError):
585 dmap = {}
589 dmap = {}
586 cache = dircache.setdefault(dir, dmap)
590 cache = dircache.setdefault(dir, dmap)
587 yield cache.get(base, None)
591 yield cache.get(base, None)
588
592
589
593
590 def username(uid: Optional[int] = None) -> Optional[bytes]:
594 def username(uid: Optional[int] = None) -> Optional[bytes]:
591 """Return the name of the user with the given uid.
595 """Return the name of the user with the given uid.
592
596
593 If uid is None, return the name of the current user."""
597 If uid is None, return the name of the current user."""
594 if not uid:
598 if not uid:
595 try:
599 try:
596 return pycompat.fsencode(getpass.getuser())
600 return pycompat.fsencode(getpass.getuser())
597 except ModuleNotFoundError:
601 except ModuleNotFoundError:
598 # getpass.getuser() checks for a few environment variables first,
602 # getpass.getuser() checks for a few environment variables first,
599 # but if those aren't set, imports pwd and calls getpwuid(), none of
603 # but if those aren't set, imports pwd and calls getpwuid(), none of
600 # which exists on Windows.
604 # which exists on Windows.
601 pass
605 pass
602 return None
606 return None
603
607
604
608
605 def groupname(gid: Optional[int] = None) -> Optional[bytes]:
609 def groupname(gid: Optional[int] = None) -> Optional[bytes]:
606 """Return the name of the group with the given gid.
610 """Return the name of the group with the given gid.
607
611
608 If gid is None, return the name of the current group."""
612 If gid is None, return the name of the current group."""
609 return None
613 return None
610
614
611
615
612 def readlink(pathname):
616 def readlink(pathname: bytes) -> bytes:
613 path = pycompat.fsdecode(pathname)
617 path = pycompat.fsdecode(pathname)
614 try:
618 try:
615 link = os.readlink(path)
619 link = os.readlink(path)
616 except ValueError as e:
620 except ValueError as e:
617 # On py2, os.readlink() raises an AttributeError since it is
621 # On py2, os.readlink() raises an AttributeError since it is
618 # unsupported. On py3, reading a non-link raises a ValueError. Simply
622 # unsupported. On py3, reading a non-link raises a ValueError. Simply
619 # treat this as the error the locking code has been expecting up to now
623 # treat this as the error the locking code has been expecting up to now
620 # until an effort can be made to enable symlink support on Windows.
624 # until an effort can be made to enable symlink support on Windows.
621 raise AttributeError(e)
625 raise AttributeError(e)
622 return pycompat.fsencode(link)
626 return pycompat.fsencode(link)
623
627
624
628
625 def removedirs(name):
629 def removedirs(name: bytes) -> None:
626 """special version of os.removedirs that does not remove symlinked
630 """special version of os.removedirs that does not remove symlinked
627 directories or junction points if they actually contain files"""
631 directories or junction points if they actually contain files"""
628 if listdir(name):
632 if listdir(name):
629 return
633 return
630 os.rmdir(name)
634 os.rmdir(name)
631 head, tail = os.path.split(name)
635 head, tail = os.path.split(name)
632 if not tail:
636 if not tail:
633 head, tail = os.path.split(head)
637 head, tail = os.path.split(head)
634 while head and tail:
638 while head and tail:
635 try:
639 try:
636 if listdir(head):
640 if listdir(head):
637 return
641 return
638 os.rmdir(head)
642 os.rmdir(head)
639 except (ValueError, OSError):
643 except (ValueError, OSError):
640 break
644 break
641 head, tail = os.path.split(head)
645 head, tail = os.path.split(head)
642
646
643
647
644 def rename(src, dst):
648 def rename(src: bytes, dst: bytes) -> None:
645 '''atomically rename file src to dst, replacing dst if it exists'''
649 '''atomically rename file src to dst, replacing dst if it exists'''
646 try:
650 try:
647 os.rename(src, dst)
651 os.rename(src, dst)
648 except FileExistsError:
652 except FileExistsError:
649 unlink(dst)
653 unlink(dst)
650 os.rename(src, dst)
654 os.rename(src, dst)
651
655
652
656
653 def gethgcmd():
657 def gethgcmd() -> List[bytes]:
654 return [encoding.strtolocal(arg) for arg in [sys.executable] + sys.argv[:1]]
658 return [encoding.strtolocal(arg) for arg in [sys.executable] + sys.argv[:1]]
655
659
656
660
657 def groupmembers(name: bytes) -> List[bytes]:
661 def groupmembers(name: bytes) -> List[bytes]:
658 # Don't support groups on Windows for now
662 # Don't support groups on Windows for now
659 raise KeyError
663 raise KeyError
660
664
661
665
662 def isexec(f: bytes) -> bool:
666 def isexec(f: bytes) -> bool:
663 return False
667 return False
664
668
665
669
666 class cachestat:
670 class cachestat:
667 def __init__(self, path: bytes) -> None:
671 def __init__(self, path: bytes) -> None:
668 pass
672 pass
669
673
670 def cacheable(self) -> bool:
674 def cacheable(self) -> bool:
671 return False
675 return False
672
676
673
677
674 def lookupreg(
678 def lookupreg(
675 key: bytes,
679 key: bytes,
676 valname: Optional[bytes] = None,
680 valname: Optional[bytes] = None,
677 scope: Optional[Union[int, Iterable[int]]] = None,
681 scope: Optional[Union[int, Iterable[int]]] = None,
678 ) -> Optional[bytes]:
682 ) -> Optional[bytes]:
679 """Look up a key/value name in the Windows registry.
683 """Look up a key/value name in the Windows registry.
680
684
681 valname: value name. If unspecified, the default value for the key
685 valname: value name. If unspecified, the default value for the key
682 is used.
686 is used.
683 scope: optionally specify scope for registry lookup, this can be
687 scope: optionally specify scope for registry lookup, this can be
684 a sequence of scopes to look up in order. Default (CURRENT_USER,
688 a sequence of scopes to look up in order. Default (CURRENT_USER,
685 LOCAL_MACHINE).
689 LOCAL_MACHINE).
686 """
690 """
687 if scope is None:
691 if scope is None:
688 # pytype: disable=module-attr
692 # pytype: disable=module-attr
689 scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
693 scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
690 # pytype: enable=module-attr
694 # pytype: enable=module-attr
691 elif not isinstance(scope, (list, tuple)):
695 elif not isinstance(scope, (list, tuple)):
692 scope = (scope,)
696 scope = (scope,)
693 for s in scope:
697 for s in scope:
694 try:
698 try:
695 # pytype: disable=module-attr
699 # pytype: disable=module-attr
696 with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey:
700 with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey:
697 # pytype: enable=module-attr
701 # pytype: enable=module-attr
698 name = None
702 name = None
699 if valname is not None:
703 if valname is not None:
700 name = encoding.strfromlocal(valname)
704 name = encoding.strfromlocal(valname)
701 # pytype: disable=module-attr
705 # pytype: disable=module-attr
702 val = winreg.QueryValueEx(hkey, name)[0]
706 val = winreg.QueryValueEx(hkey, name)[0]
703 # pytype: enable=module-attr
707 # pytype: enable=module-attr
704
708
705 # never let a Unicode string escape into the wild
709 # never let a Unicode string escape into the wild
706 return encoding.unitolocal(val)
710 return encoding.unitolocal(val)
707 except EnvironmentError:
711 except EnvironmentError:
708 pass
712 pass
709
713
710
714
711 expandglobs = True
715 expandglobs: bool = True
712
716
713
717
714 def statislink(st: Optional[os.stat_result]) -> bool:
718 def statislink(st: Optional[os.stat_result]) -> bool:
715 '''check whether a stat result is a symlink'''
719 '''check whether a stat result is a symlink'''
716 return False
720 return False
717
721
718
722
719 def statisexec(st: Optional[os.stat_result]) -> bool:
723 def statisexec(st: Optional[os.stat_result]) -> bool:
720 '''check whether a stat result is an executable file'''
724 '''check whether a stat result is an executable file'''
721 return False
725 return False
722
726
723
727
724 def poll(fds):
728 def poll(fds) -> List:
725 # see posix.py for description
729 # see posix.py for description
726 raise NotImplementedError()
730 raise NotImplementedError()
727
731
728
732
729 def readpipe(pipe) -> bytes:
733 def readpipe(pipe) -> bytes:
730 """Read all available data from a pipe."""
734 """Read all available data from a pipe."""
731 chunks = []
735 chunks = []
732 while True:
736 while True:
733 size = win32.peekpipe(pipe)
737 size = win32.peekpipe(pipe)
734 if not size:
738 if not size:
735 break
739 break
736
740
737 s = pipe.read(size)
741 s = pipe.read(size)
738 if not s:
742 if not s:
739 break
743 break
740 chunks.append(s)
744 chunks.append(s)
741
745
742 return b''.join(chunks)
746 return b''.join(chunks)
743
747
744
748
745 def bindunixsocket(sock, path: bytes) -> NoReturn:
749 def bindunixsocket(sock, path: bytes) -> NoReturn:
746 raise NotImplementedError('unsupported platform')
750 raise NotImplementedError('unsupported platform')
General Comments 0
You need to be logged in to leave comments. Login now