##// END OF EJS Templates
windows: add a method to convert Unix style command lines to Windows style...
Matt Harbison -
r38502:3efadf23 default
parent child Browse files
Show More
@@ -1,485 +1,588 b''
1 1 # windows.py - Windows utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import msvcrt
12 12 import os
13 13 import re
14 14 import stat
15 import string
15 16 import sys
16 17
17 18 from .i18n import _
18 19 from . import (
19 20 encoding,
20 21 error,
21 22 policy,
22 23 pycompat,
23 24 win32,
24 25 )
25 26
26 27 try:
27 28 import _winreg as winreg
28 29 winreg.CloseKey
29 30 except ImportError:
30 31 import winreg
31 32
32 33 osutil = policy.importmod(r'osutil')
33 34
34 35 getfsmountpoint = win32.getvolumename
35 36 getfstype = win32.getfstype
36 37 getuser = win32.getuser
37 38 hidewindow = win32.hidewindow
38 39 makedir = win32.makedir
39 40 nlinks = win32.nlinks
40 41 oslink = win32.oslink
41 42 samedevice = win32.samedevice
42 43 samefile = win32.samefile
43 44 setsignalhandler = win32.setsignalhandler
44 45 spawndetached = win32.spawndetached
45 46 split = os.path.split
46 47 testpid = win32.testpid
47 48 unlink = win32.unlink
48 49
49 50 umask = 0o022
50 51
51 52 class mixedfilemodewrapper(object):
52 53 """Wraps a file handle when it is opened in read/write mode.
53 54
54 55 fopen() and fdopen() on Windows have a specific-to-Windows requirement
55 56 that files opened with mode r+, w+, or a+ make a call to a file positioning
56 57 function when switching between reads and writes. Without this extra call,
57 58 Python will raise a not very intuitive "IOError: [Errno 0] Error."
58 59
59 60 This class wraps posixfile instances when the file is opened in read/write
60 61 mode and automatically adds checks or inserts appropriate file positioning
61 62 calls when necessary.
62 63 """
63 64 OPNONE = 0
64 65 OPREAD = 1
65 66 OPWRITE = 2
66 67
67 68 def __init__(self, fp):
68 69 object.__setattr__(self, r'_fp', fp)
69 70 object.__setattr__(self, r'_lastop', 0)
70 71
71 72 def __enter__(self):
72 73 return self._fp.__enter__()
73 74
74 75 def __exit__(self, exc_type, exc_val, exc_tb):
75 76 self._fp.__exit__(exc_type, exc_val, exc_tb)
76 77
77 78 def __getattr__(self, name):
78 79 return getattr(self._fp, name)
79 80
80 81 def __setattr__(self, name, value):
81 82 return self._fp.__setattr__(name, value)
82 83
83 84 def _noopseek(self):
84 85 self._fp.seek(0, os.SEEK_CUR)
85 86
86 87 def seek(self, *args, **kwargs):
87 88 object.__setattr__(self, r'_lastop', self.OPNONE)
88 89 return self._fp.seek(*args, **kwargs)
89 90
90 91 def write(self, d):
91 92 if self._lastop == self.OPREAD:
92 93 self._noopseek()
93 94
94 95 object.__setattr__(self, r'_lastop', self.OPWRITE)
95 96 return self._fp.write(d)
96 97
97 98 def writelines(self, *args, **kwargs):
98 99 if self._lastop == self.OPREAD:
99 100 self._noopeseek()
100 101
101 102 object.__setattr__(self, r'_lastop', self.OPWRITE)
102 103 return self._fp.writelines(*args, **kwargs)
103 104
104 105 def read(self, *args, **kwargs):
105 106 if self._lastop == self.OPWRITE:
106 107 self._noopseek()
107 108
108 109 object.__setattr__(self, r'_lastop', self.OPREAD)
109 110 return self._fp.read(*args, **kwargs)
110 111
111 112 def readline(self, *args, **kwargs):
112 113 if self._lastop == self.OPWRITE:
113 114 self._noopseek()
114 115
115 116 object.__setattr__(self, r'_lastop', self.OPREAD)
116 117 return self._fp.readline(*args, **kwargs)
117 118
118 119 def readlines(self, *args, **kwargs):
119 120 if self._lastop == self.OPWRITE:
120 121 self._noopseek()
121 122
122 123 object.__setattr__(self, r'_lastop', self.OPREAD)
123 124 return self._fp.readlines(*args, **kwargs)
124 125
125 126 def posixfile(name, mode='r', buffering=-1):
126 127 '''Open a file with even more POSIX-like semantics'''
127 128 try:
128 129 fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError
129 130
130 131 # The position when opening in append mode is implementation defined, so
131 132 # make it consistent with other platforms, which position at EOF.
132 133 if 'a' in mode:
133 134 fp.seek(0, os.SEEK_END)
134 135
135 136 if '+' in mode:
136 137 return mixedfilemodewrapper(fp)
137 138
138 139 return fp
139 140 except WindowsError as err:
140 141 # convert to a friendlier exception
141 142 raise IOError(err.errno, '%s: %s' % (
142 143 name, encoding.strtolocal(err.strerror)))
143 144
144 145 # may be wrapped by win32mbcs extension
145 146 listdir = osutil.listdir
146 147
147 148 class winstdout(object):
148 149 '''stdout on windows misbehaves if sent through a pipe'''
149 150
150 151 def __init__(self, fp):
151 152 self.fp = fp
152 153
153 154 def __getattr__(self, key):
154 155 return getattr(self.fp, key)
155 156
156 157 def close(self):
157 158 try:
158 159 self.fp.close()
159 160 except IOError:
160 161 pass
161 162
162 163 def write(self, s):
163 164 try:
164 165 # This is workaround for "Not enough space" error on
165 166 # writing large size of data to console.
166 167 limit = 16000
167 168 l = len(s)
168 169 start = 0
169 170 self.softspace = 0
170 171 while start < l:
171 172 end = start + limit
172 173 self.fp.write(s[start:end])
173 174 start = end
174 175 except IOError as inst:
175 176 if inst.errno != 0:
176 177 raise
177 178 self.close()
178 179 raise IOError(errno.EPIPE, 'Broken pipe')
179 180
180 181 def flush(self):
181 182 try:
182 183 return self.fp.flush()
183 184 except IOError as inst:
184 185 if inst.errno != errno.EINVAL:
185 186 raise
186 187 raise IOError(errno.EPIPE, 'Broken pipe')
187 188
188 189 def _is_win_9x():
189 190 '''return true if run on windows 95, 98 or me.'''
190 191 try:
191 192 return sys.getwindowsversion()[3] == 1
192 193 except AttributeError:
193 194 return 'command' in encoding.environ.get('comspec', '')
194 195
195 196 def openhardlinks():
196 197 return not _is_win_9x()
197 198
198 199 def parsepatchoutput(output_line):
199 200 """parses the output produced by patch and returns the filename"""
200 201 pf = output_line[14:]
201 202 if pf[0] == '`':
202 203 pf = pf[1:-1] # Remove the quotes
203 204 return pf
204 205
205 206 def sshargs(sshcmd, host, user, port):
206 207 '''Build argument list for ssh or Plink'''
207 208 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
208 209 args = user and ("%s@%s" % (user, host)) or host
209 210 if args.startswith('-') or args.startswith('/'):
210 211 raise error.Abort(
211 212 _('illegal ssh hostname or username starting with - or /: %s') %
212 213 args)
213 214 args = shellquote(args)
214 215 if port:
215 216 args = '%s %s %s' % (pflag, shellquote(port), args)
216 217 return args
217 218
218 219 def setflags(f, l, x):
219 220 pass
220 221
221 222 def copymode(src, dst, mode=None):
222 223 pass
223 224
224 225 def checkexec(path):
225 226 return False
226 227
227 228 def checklink(path):
228 229 return False
229 230
230 231 def setbinary(fd):
231 232 # When run without console, pipes may expose invalid
232 233 # fileno(), usually set to -1.
233 234 fno = getattr(fd, 'fileno', None)
234 235 if fno is not None and fno() >= 0:
235 236 msvcrt.setmode(fno(), os.O_BINARY)
236 237
237 238 def pconvert(path):
238 239 return path.replace(pycompat.ossep, '/')
239 240
240 241 def localpath(path):
241 242 return path.replace('/', '\\')
242 243
243 244 def normpath(path):
244 245 return pconvert(os.path.normpath(path))
245 246
246 247 def normcase(path):
247 248 return encoding.upper(path) # NTFS compares via upper()
248 249
249 250 # see posix.py for definitions
250 251 normcasespec = encoding.normcasespecs.upper
251 252 normcasefallback = encoding.upperfallback
252 253
253 254 def samestat(s1, s2):
254 255 return False
255 256
257 def shelltocmdexe(path, env):
258 r"""Convert shell variables in the form $var and ${var} inside ``path``
259 to %var% form. Existing Windows style variables are left unchanged.
260
261 The variables are limited to the given environment. Unknown variables are
262 left unchanged.
263
264 >>> e = {b'var1': b'v1', b'var2': b'v2', b'var3': b'v3'}
265 >>> # Only valid values are expanded
266 >>> shelltocmdexe(b'cmd $var1 ${var2} %var3% $missing ${missing} %missing%',
267 ... e)
268 'cmd %var1% %var2% %var3% $missing ${missing} %missing%'
269 >>> # Single quote prevents expansion, as does \$ escaping
270 >>> shelltocmdexe(b"cmd '$var1 ${var2} %var3%' \$var1 \${var2} \\", e)
271 "cmd '$var1 ${var2} %var3%' $var1 ${var2} \\"
272 >>> # $$ -> $, %% is not special, but can be the end and start of variables
273 >>> shelltocmdexe(b"cmd $$ %% %var1%%var2%", e)
274 'cmd $ %% %var1%%var2%'
275 >>> # No double substitution
276 >>> shelltocmdexe(b"$var1 %var1%", {b'var1': b'%var2%', b'var2': b'boom'})
277 '%var1% %var1%'
278 """
279 if b'$' not in path:
280 return path
281
282 varchars = pycompat.sysbytes(string.ascii_letters + string.digits) + b'_-'
283
284 res = b''
285 index = 0
286 pathlen = len(path)
287 while index < pathlen:
288 c = path[index]
289 if c == b'\'': # no expansion within single quotes
290 path = path[index + 1:]
291 pathlen = len(path)
292 try:
293 index = path.index(b'\'')
294 res += b'\'' + path[:index + 1]
295 except ValueError:
296 res += c + path
297 index = pathlen - 1
298 elif c == b'%': # variable
299 path = path[index + 1:]
300 pathlen = len(path)
301 try:
302 index = path.index(b'%')
303 except ValueError:
304 res += b'%' + path
305 index = pathlen - 1
306 else:
307 var = path[:index]
308 res += b'%' + var + b'%'
309 elif c == b'$': # variable or '$$'
310 if path[index + 1:index + 2] == b'$':
311 res += c
312 index += 1
313 elif path[index + 1:index + 2] == b'{':
314 path = path[index + 2:]
315 pathlen = len(path)
316 try:
317 index = path.index(b'}')
318 var = path[:index]
319
320 # See below for why empty variables are handled specially
321 if env.get(var, '') != '':
322 res += b'%' + var + b'%'
323 else:
324 res += b'${' + var + b'}'
325 except ValueError:
326 res += b'${' + path
327 index = pathlen - 1
328 else:
329 var = b''
330 index += 1
331 c = path[index:index + 1]
332 while c != b'' and c in varchars:
333 var += c
334 index += 1
335 c = path[index:index + 1]
336 # Some variables (like HG_OLDNODE) may be defined, but have an
337 # empty value. Those need to be skipped because when spawning
338 # cmd.exe to run the hook, it doesn't replace %VAR% for an empty
339 # VAR, and that really confuses things like revset expressions.
340 # OTOH, if it's left in Unix format and the hook runs sh.exe, it
341 # will substitute to an empty string, and everything is happy.
342 if env.get(var, '') != '':
343 res += b'%' + var + b'%'
344 else:
345 res += b'$' + var
346
347 if c != '':
348 index -= 1
349 elif c == b'\\' and index + 1 < pathlen and path[index + 1] == b'$':
350 # Skip '\', but only if it is escaping $
351 res += b'$'
352 index += 1
353 else:
354 res += c
355
356 index += 1
357 return res
358
256 359 # A sequence of backslashes is special iff it precedes a double quote:
257 360 # - if there's an even number of backslashes, the double quote is not
258 361 # quoted (i.e. it ends the quoted region)
259 362 # - if there's an odd number of backslashes, the double quote is quoted
260 363 # - in both cases, every pair of backslashes is unquoted into a single
261 364 # backslash
262 365 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
263 366 # So, to quote a string, we must surround it in double quotes, double
264 367 # the number of backslashes that precede double quotes and add another
265 368 # backslash before every double quote (being careful with the double
266 369 # quote we've appended to the end)
267 370 _quotere = None
268 371 _needsshellquote = None
269 372 def shellquote(s):
270 373 r"""
271 374 >>> shellquote(br'C:\Users\xyz')
272 375 '"C:\\Users\\xyz"'
273 376 >>> shellquote(br'C:\Users\xyz/mixed')
274 377 '"C:\\Users\\xyz/mixed"'
275 378 >>> # Would be safe not to quote too, since it is all double backslashes
276 379 >>> shellquote(br'C:\\Users\\xyz')
277 380 '"C:\\\\Users\\\\xyz"'
278 381 >>> # But this must be quoted
279 382 >>> shellquote(br'C:\\Users\\xyz/abc')
280 383 '"C:\\\\Users\\\\xyz/abc"'
281 384 """
282 385 global _quotere
283 386 if _quotere is None:
284 387 _quotere = re.compile(r'(\\*)("|\\$)')
285 388 global _needsshellquote
286 389 if _needsshellquote is None:
287 390 # ":" is also treated as "safe character", because it is used as a part
288 391 # of path name on Windows. "\" is also part of a path name, but isn't
289 392 # safe because shlex.split() (kind of) treats it as an escape char and
290 393 # drops it. It will leave the next character, even if it is another
291 394 # "\".
292 395 _needsshellquote = re.compile(r'[^a-zA-Z0-9._:/-]').search
293 396 if s and not _needsshellquote(s) and not _quotere.search(s):
294 397 # "s" shouldn't have to be quoted
295 398 return s
296 399 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
297 400
298 401 def _unquote(s):
299 402 if s.startswith(b'"') and s.endswith(b'"'):
300 403 return s[1:-1]
301 404 return s
302 405
303 406 def shellsplit(s):
304 407 """Parse a command string in cmd.exe way (best-effort)"""
305 408 return pycompat.maplist(_unquote, pycompat.shlexsplit(s, posix=False))
306 409
307 410 def quotecommand(cmd):
308 411 """Build a command string suitable for os.popen* calls."""
309 412 if sys.version_info < (2, 7, 1):
310 413 # Python versions since 2.7.1 do this extra quoting themselves
311 414 return '"' + cmd + '"'
312 415 return cmd
313 416
314 417 # if you change this stub into a real check, please try to implement the
315 418 # username and groupname functions above, too.
316 419 def isowner(st):
317 420 return True
318 421
319 422 def findexe(command):
320 423 '''Find executable for command searching like cmd.exe does.
321 424 If command is a basename then PATH is searched for command.
322 425 PATH isn't searched if command is an absolute or relative path.
323 426 An extension from PATHEXT is found and added if not present.
324 427 If command isn't found None is returned.'''
325 428 pathext = encoding.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
326 429 pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)]
327 430 if os.path.splitext(command)[1].lower() in pathexts:
328 431 pathexts = ['']
329 432
330 433 def findexisting(pathcommand):
331 434 'Will append extension (if needed) and return existing file'
332 435 for ext in pathexts:
333 436 executable = pathcommand + ext
334 437 if os.path.exists(executable):
335 438 return executable
336 439 return None
337 440
338 441 if pycompat.ossep in command:
339 442 return findexisting(command)
340 443
341 444 for path in encoding.environ.get('PATH', '').split(pycompat.ospathsep):
342 445 executable = findexisting(os.path.join(path, command))
343 446 if executable is not None:
344 447 return executable
345 448 return findexisting(os.path.expanduser(os.path.expandvars(command)))
346 449
347 450 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
348 451
349 452 def statfiles(files):
350 453 '''Stat each file in files. Yield each stat, or None if a file
351 454 does not exist or has a type we don't care about.
352 455
353 456 Cluster and cache stat per directory to minimize number of OS stat calls.'''
354 457 dircache = {} # dirname -> filename -> status | None if file does not exist
355 458 getkind = stat.S_IFMT
356 459 for nf in files:
357 460 nf = normcase(nf)
358 461 dir, base = os.path.split(nf)
359 462 if not dir:
360 463 dir = '.'
361 464 cache = dircache.get(dir, None)
362 465 if cache is None:
363 466 try:
364 467 dmap = dict([(normcase(n), s)
365 468 for n, k, s in listdir(dir, True)
366 469 if getkind(s.st_mode) in _wantedkinds])
367 470 except OSError as err:
368 471 # Python >= 2.5 returns ENOENT and adds winerror field
369 472 # EINVAL is raised if dir is not a directory.
370 473 if err.errno not in (errno.ENOENT, errno.EINVAL,
371 474 errno.ENOTDIR):
372 475 raise
373 476 dmap = {}
374 477 cache = dircache.setdefault(dir, dmap)
375 478 yield cache.get(base, None)
376 479
377 480 def username(uid=None):
378 481 """Return the name of the user with the given uid.
379 482
380 483 If uid is None, return the name of the current user."""
381 484 return None
382 485
383 486 def groupname(gid=None):
384 487 """Return the name of the group with the given gid.
385 488
386 489 If gid is None, return the name of the current group."""
387 490 return None
388 491
389 492 def removedirs(name):
390 493 """special version of os.removedirs that does not remove symlinked
391 494 directories or junction points if they actually contain files"""
392 495 if listdir(name):
393 496 return
394 497 os.rmdir(name)
395 498 head, tail = os.path.split(name)
396 499 if not tail:
397 500 head, tail = os.path.split(head)
398 501 while head and tail:
399 502 try:
400 503 if listdir(head):
401 504 return
402 505 os.rmdir(head)
403 506 except (ValueError, OSError):
404 507 break
405 508 head, tail = os.path.split(head)
406 509
407 510 def rename(src, dst):
408 511 '''atomically rename file src to dst, replacing dst if it exists'''
409 512 try:
410 513 os.rename(src, dst)
411 514 except OSError as e:
412 515 if e.errno != errno.EEXIST:
413 516 raise
414 517 unlink(dst)
415 518 os.rename(src, dst)
416 519
417 520 def gethgcmd():
418 521 return [sys.executable] + sys.argv[:1]
419 522
420 523 def groupmembers(name):
421 524 # Don't support groups on Windows for now
422 525 raise KeyError
423 526
424 527 def isexec(f):
425 528 return False
426 529
427 530 class cachestat(object):
428 531 def __init__(self, path):
429 532 pass
430 533
431 534 def cacheable(self):
432 535 return False
433 536
434 537 def lookupreg(key, valname=None, scope=None):
435 538 ''' Look up a key/value name in the Windows registry.
436 539
437 540 valname: value name. If unspecified, the default value for the key
438 541 is used.
439 542 scope: optionally specify scope for registry lookup, this can be
440 543 a sequence of scopes to look up in order. Default (CURRENT_USER,
441 544 LOCAL_MACHINE).
442 545 '''
443 546 if scope is None:
444 547 scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
445 548 elif not isinstance(scope, (list, tuple)):
446 549 scope = (scope,)
447 550 for s in scope:
448 551 try:
449 552 val = winreg.QueryValueEx(winreg.OpenKey(s, key), valname)[0]
450 553 # never let a Unicode string escape into the wild
451 554 return encoding.unitolocal(val)
452 555 except EnvironmentError:
453 556 pass
454 557
455 558 expandglobs = True
456 559
457 560 def statislink(st):
458 561 '''check whether a stat result is a symlink'''
459 562 return False
460 563
461 564 def statisexec(st):
462 565 '''check whether a stat result is an executable file'''
463 566 return False
464 567
465 568 def poll(fds):
466 569 # see posix.py for description
467 570 raise NotImplementedError()
468 571
469 572 def readpipe(pipe):
470 573 """Read all available data from a pipe."""
471 574 chunks = []
472 575 while True:
473 576 size = win32.peekpipe(pipe)
474 577 if not size:
475 578 break
476 579
477 580 s = pipe.read(size)
478 581 if not s:
479 582 break
480 583 chunks.append(s)
481 584
482 585 return ''.join(chunks)
483 586
484 587 def bindunixsocket(sock, path):
485 588 raise NotImplementedError('unsupported platform')
General Comments 0
You need to be logged in to leave comments. Login now