Преобразовать POSIX-> путь WIN, в Cygwin Python, без вызова cygpath

Я использую Python script, работающий в Cygwin-сборке Python, для создания команд, выпущенных для родных утилит Windows (не Cygwin-aware). Это требует преобразования параметров пути из POSIX в форму WIN перед выдачей команды.

Вызов утилиты cygpath - это лучший способ сделать это, поскольку он использует Cygwin для выполнения того, что он должен делать, но также немного ужасающий (и медленный).

Я уже запускаю Cygwin-сборку Python - так что код для преобразования присутствует. Похоже, что должно существовать специальное расширение Cygwin/Python, которое дает мне возможность подключиться к этой возможности прямо на Python без необходимости запуска совершенно нового процесса.

Ответ 1

Это возможно, вызывая API Cygwin с помощью ctypes. Следующий код работает для меня - я использую 64-разрядную версию cygwin DLL версии 2.5.2 в Windows 2012, и это работает на версиях Cygwin как Python 2.7.10, так и Python 3.4.3.

В основном мы вызываем cygwin_create_path из cygwin1.dll для выполнения преобразования пути. Эта функция выделяет буфер памяти (используя malloc), содержащий преобразованный путь. Итак, нам нужно использовать free из cygwin1.dll, чтобы освободить выделенный буфер.

Обратите внимание, что xunicode ниже - это бедный человек, альтернативный six (библиотека совместимости Python 2/3); если вам нужно поддерживать оба Python 2 и 3, шесть - лучший ответ, но я хотел, чтобы мой пример был свободен от зависимостей от любых несвязанных модулей, поэтому я сделал это таким образом.

from ctypes import cdll, c_void_p, c_int32, cast, c_char_p, c_wchar_p
from sys import version_info

xunicode = str if version_info[0] > 2 else eval("unicode")

# If running under Cygwin Python, just use DLL name
# If running under non-Cygwin Windows Python, use full path to cygwin1.dll
# Note Python and cygwin1.dll must match bitness (i.e. 32-bit Python must
# use 32-bit cygwin1.dll, 64-bit Python must use 64-bit cygwin1.dll.)
cygwin = cdll.LoadLibrary("cygwin1.dll")
cygwin_create_path = cygwin.cygwin_create_path
cygwin_create_path.restype = c_void_p
cygwin_create_path.argtypes = [c_int32, c_void_p]

# Initialise the cygwin DLL. This step should only be done if using
# non-Cygwin Python. If you are using Cygwin Python don't do this because
# it has already been done for you.
cygwin_dll_init = cygwin.cygwin_dll_init
cygwin_dll_init.restype = None
cygwin_dll_init.argtypes = []
cygwin_dll_init()

free = cygwin.free
free.restype = None
free.argtypes = [c_void_p]

CCP_POSIX_TO_WIN_A = 0
CCP_POSIX_TO_WIN_W = 1
CCP_WIN_A_TO_POSIX = 2
CCP_WIN_W_TO_POSIX = 3

def win2posix(path):
    """Convert a Windows path to a Cygwin path"""
    result = cygwin_create_path(CCP_WIN_W_TO_POSIX,xunicode(path))
    if result is None:
        raise Exception("cygwin_create_path failed")
    value = cast(result,c_char_p).value
    free(result)
    return value

def posix2win(path):
    """Convert a Cygwin path to a Windows path"""
    result = cygwin_create_path(CCP_POSIX_TO_WIN_W,str(path))
    if result is None:
        raise Exception("cygwin_create_path failed")
    value = cast(result,c_wchar_p).value
    free(result)
    return value

# Example, convert LOCALAPPDATA to cygwin path and back
from os import environ
localAppData = environ["LOCALAPPDATA"]
print("Original Win32 path: %s" % localAppData)
localAppData = win2posix(localAppData)
print("As a POSIX path: %s" % localAppData)
localAppData = posix2win(localAppData)
print("Back to a Windows path: %s" % localAppData)

Ответ 2

Из просмотра источника cygpath, похоже, что cygpath имеет нетривиальную реализацию и не делает доступной версию библиотеки.

cygpath поддерживает ввод своих данных из файла с помощью параметра -f (или из stdin, используя -f -), и может принимать несколько путей, каждый раз выплескивая преобразованный путь, так что вы, вероятно, могли бы создать единый cygpath instance open (используя Python subprocess.Popen), а не перезапускать cygpath каждый раз.

Ответ 3

Я бы предпочел написать этот помощник Python, который использует cygwin dll:

import errno
import ctypes
import enum
import sys

class ccp_what(enum.Enum):
    posix_to_win_a = 0 # from is char *posix, to is char *win32
    posix_to_win_w = 1 # from is char *posix, to is wchar_t *win32
    win_a_to_posix = 2 # from is char *win32, to is char *posix
    win_w_to_posix = 3 # from is wchar_t *win32, to is char *posix

    convtype_mask = 3

    absolute = 0          # Request absolute path (default).
    relative = 0x100      # Request to keep path relative.
    proc_cygdrive = 0x200 # Request to return /proc/cygdrive path (only with CCP_*_TO_POSIX)

class CygpathError(Exception):
    def __init__(self, errno, msg=""):
        self.errno = errno
        super(Exception, self).__init__(os.strerror(errno))

class Cygpath(object):
    bufsize = 512

    def __init__(self):
        if 'cygwin' not in sys.platform:
            raise SystemError('Not running on cygwin')

        self._dll = ctypes.cdll.LoadLibrary("cygwin1.dll")

    def _cygwin_conv_path(self, what, path, size = None):
        if size is None:
            size = self.bufsize
        out = ctypes.create_string_buffer(size)
        ret = self._dll.cygwin_conv_path(what, path, out, size)
        if ret < 0:
            raise CygpathError(ctypes.get_errno())
        return out.value

    def posix2win(self, path, relative=False):
        out = ctypes.create_string_buffer(self.bufsize)
        t = ccp_what.relative.value if relative else ccp_what.absolute.value
        what = ccp_what.posix_to_win_a.value | t
        return self._cygwin_conv_path(what, path)

    def win2posix(self, path, relative=False):
        out = ctypes.create_string_buffer(self.bufsize)
        t = ccp_what.relative.value if relative else ccp_what.absolute.value
        what = ccp_what.win_a_to_posix.value | t
        return self._cygwin_conv_path(what, path)

Ответ 4

Я недавно столкнулся с этой проблемой самостоятельно. Небольшое и быстрое решение, которое я нашел, заключается в следующем:

import os
import re

def win_path(path):
    match = re.match('(/(cygdrive/)?)(.*)', path)
    if not match:
        return path.replace('/', '\\')
    dirs = match.group(3).split('/')
    dirs[0] = f'{dirs[0].upper()}:'
    return '\\'.join(dirs)

Это работает как с путями в стиле cygwin (/cygdrive/...), так и с MinGW (/...) (я должен был поддерживать оба), а также с относительными путями.

l = ['/c/test/path',
     '/cygdrive/c/test/path',
     './test/path',
     '../test/path',
     'C:\Windows\Path',
     '.\Windows\Path',
     '..\Windows\Path']
for i in l:
    print(win_path(i))

Производит:

C:\test\path
C:\test\path
.\test\path
..\test\path
C:\Windows\Path
.\Windows\Path
..\Windows\Path