YOGYUI

Python - 윈도OS 환경 변수 변경하기 (Modify Environment Variables) 본문

Software/Python

Python - 윈도OS 환경 변수 변경하기 (Modify Environment Variables)

요겨 2021. 8. 17. 20:17
반응형

 

윈도 OS를 사용하다보면 (특히 개발자들은) 환경 변수를 설정해야하는 경우가 있다

제어판 - 시스템 - 고급 탭의 '환경 변수' GUI로 해결하는게 일반적이다

 

예시를 위해 'TEST' 변수를 한 개 만든 뒤에 Python으로 수정하는 방법을 알아보자

(이 글에서는 사용자 변수가 아니라 시스템 변수를 대상으로 한다)

임의로 C:\test1, C:\test2, C:\test3 3개의 경로를 값으로 입력했다

(실제 존재하지 않는 경로여도 상관없다)

여러 개의 경로를 입력할 경우 세미콜론(;) 구분자로 입력해줘야 한다

1. OS module

Python 구동 시 환경변수를 동적으로 변경하고자 할 경우 built-in 모듈인 osenviron (_Environ) 클래스를 사용하는게 일반적인 방법이다

 

'TEST' 변수값을 가져오려면 딕셔너리를 사용하는 것과 동일하게 get 메서드를 사용하면 된다

(변수명을 딕셔너리의 key 값처럼 사용)

import os

test = os.environ.get('TEST')  # 대소문자 구분 없음
# test = os.environ['TEST']  # 결과는 동일
In [1]: print(test)
Out[1]: C:\\test1;C:\\test2;C:\\test3

In [2]: type(test)
Out[2]: str

문자열 객체(str)로 반환되며, 경로 문자열로 구분하려면 다음과 같이 작성하면 된다

test_paths = test.split(';')
In [3]: print(test_paths)
Out[3]: ['C:\\test1', 'C:\\test2', 'C:\\test3']

 

 

C:\test4 경로를 하나 추가해보자

test_paths.append("C:\\test4")
os.environ['TEST'] = ';'.join(test_paths)
In [4]: os.environ['TEST']
Out[4]: C:\\test1;C:\\test2;C:\\test3;C:\\test4

아주 쉽게 추가할 수 있다

 

하지만!

제어판을 다시 열어보면 시스템 변수는 추가되지 않은 것을 알 수 있다

또한, 위 스크립트를 다시 실행해보면 C:\test4 경로는 처음에는 TEST 변수 내에 존재하지 않는다

 

즉, os 모듈의 environ으로는 시스템 변수를 영구적(permanently)으로 변경할 수 없으며, 스크립트가 돌아가는 환경 (IDE, Shell 등)이 구동될 때 로드된 일종의 '자식' 환경에서만 동적으로 변경이 되고 부모 환경은 접근할 수 없다

 

2. 레지스트리 변경하기

환경변수는 윈도우 레지스트리로 관리된다

레지스트리 편집기를 열어보자 (시작 - 실행 - regedit)

 

시스템 환경변수의 레지스트리 경로는 다음과 같다

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment

 

Python으로 윈도우 레지스트리에 접근하고자 할 때 가장 손쉬운 방법인 built-in 모듈 winreg를 사용해서 환경변수를 수정해보자

winreg의 자세한 사용법은 공식 도큐먼트 참고

https://docs.python.org/3/library/winreg.html

 

winreg — Windows registry access — Python 3.9.6 documentation

winreg — Windows registry access These functions expose the Windows registry API to Python. Instead of using an integer as the registry handle, a handle object is used to ensure that the handles are closed correctly, even if the programmer neglects to ex

docs.python.org

 

스크립트의 파일명은 registry.py로 지정

import winreg

name = 'HKEY_LOCAL_MACHINE'
path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
tree = eval('winreg.%s'%name)
varname = 'TEST'

with winreg.ConnectRegistry(None, tree) as reg:
    with winreg.OpenKey(reg, path, 0, winreg.KEY_ALL_ACCESS) as key:
        value, type_id = winreg.QueryValueEx(key, varname)
        print(value, type_id)

핵심: winreg의 QueryValueEx 함수로 특정 변수의 값을 쿼리

여기서 주의할 점은, 스크립트가 '관리자 권한'에서 실행되지 않으면 Access Denied 오류가 발생한다

(winreg 사용할 때 이게 제일 번거롭다)

python registry.py
>> PermissionError: [WinError 5] 액세스가 거부되었습니다

 

IDE나 프롬프트를 관리자 권한으로 실행하면 정상적으로 'TEST' 변수값을 읽을 수 있다

python registry.py
>> C:\test1;C:\test2;C:\test3 1

print 두번째 인자 '1'은 레지스트리 종류 중 하나인 문자열 값(REG_SZ)을 가리킨다

 

쿼리하고자 하는 변수명을 임의로 변경하기 위해 다음과 같이 함수로 만들어두자

def getSystemEnvironmentValue(varname: str):
    name = 'HKEY_LOCAL_MACHINE'
    path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
    tree = eval('winreg.%s'%name)
    with winreg.ConnectRegistry(None, tree) as reg:
        with winreg.OpenKey(reg, path, 0, winreg.KEY_ALL_ACCESS) as key:
            try:
                value, type_id = winreg.QueryValueEx(key, varname)
                return value
            except FileNotFoundError:
                return '???'

 

값을 변경하는 방법도 읽는 방법과 유사하게 하면 된다

import winreg
import win32gui, win32con

name = 'HKEY_LOCAL_MACHINE'
path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
tree = eval('winreg.%s'%name)
varname = 'TEST'
newpath = r'C:\test4'
originpath = getSystemEnvironmentValue(varname)
with winreg.ConnectRegistry(None, tree) as reg:
    with winreg.OpenKey(reg, path, 0, winreg.KEY_ALL_ACCESS) as key:
        to_write = originpath + ';' + newpath
        winreg.SetValueEx(key, varname, 0, winreg.REG_EXPAND_SZ, to_write)
        win32gui.SendMessage(win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, 'Environment')

핵심: winreg의 SetValueEx 함수로 특정 변수의 값을 변경

또한, MFC의 SendMessage 함수로 실행중인 모든 프로세스에 환경변수가 변경되었음을 notify해주는 구문도 추가해주면 다른 프로세스에서도 해당 이벤트를 감지할 수 있다

 

위 스크립트를 실행 후 변수를 확인해보면

print(getSystemEnvironmentValue('TEST'))
>> C:\test1;C:\test2;C:\test3;C:\test4

동적으로 변경된 것을 확인할 수 있다

또한, 레지스트리 편집기와 제어판 환경변수에서도 값이 변경된 것을 확인할 수 있다

 

값을 쓰는 구문까지 같이 함수화해두자

import winreg
import win32gui, win32con

def getSystemEnvironmentValue(varname: str):
    name = 'HKEY_LOCAL_MACHINE'
    path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
    tree = eval('winreg.%s'%name)
    with winreg.ConnectRegistry(None, tree) as reg:
        with winreg.OpenKey(reg, path, 0, winreg.KEY_ALL_ACCESS) as key:
            try:
                value, type_id = winreg.QueryValueEx(key, varname)
                return value
            except FileNotFoundError:
                return '???'

def setSystemEnvironmentValue(varname: str, value: str):
    name = 'HKEY_LOCAL_MACHINE'
    path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
    tree = eval('winreg.%s'%name)
    with winreg.ConnectRegistry(None, tree) as reg:
        with winreg.OpenKey(reg, path, 0, winreg.KEY_ALL_ACCESS) as key:
            winreg.SetValueEx(key, varname, 0, winreg.REG_EXPAND_SZ, value)
            win32gui.SendMessage(win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, 'Environment')

시스템 환경변수는 특별한 경우가 아니라면 문자열 값을 가지는 게 정석이므로 값 속성은 REG_EXPAND_SZ로 두고 문자열을 쓸 수 있게 함수화했다

 

P.S)

  • 변수 삭제는 winreg의 DeleteValue 함수를 사용하면 된다
  • 유저 환경변수의 레지스트리 경로는 다음과 같다
    HKEY_CURRENT_USER\Environment
    유저 환경변수도 위에서 다룬 코드와 유사하게 name과 path만 변경하면 동일하게 동작한다

 

3. Extra

환경변수 중 'PATH'는 GUI를 통해 우선순위를 결정할 수 있다

우선순위를 Python으로 동적으로 변경하는 예도 알아보자

여기서는 위 환경변수에서 4번째 경로인 C:\Python38\Scripts와 5번째 경로인 C:\Python38\의 순서를 서로 바꿔보도록 한다

 

우선 위에서 구현한 함수를 사용해 'PATH' 값을 읽은 뒤 4번째, 5번째 경로를 확인해보자

path_list = getSystemEnvironmentValue('PATH').split(';')
print(path_list[3] + '\n' + path_list[4])

>> 
C:\Python38\Scripts\
C:\Python38\

이제 리스트의 4번째 요소와 5번째 요소를 swap한 뒤, 다시 문자열로 만들어 'PATH'에 값을 입력해보자

path_list[3], path_list[4] = path_list[4], path_list[3]
to_write = ';'.join(path_list)
setSystemEnvironmentValue('PATH', to_write)

다시 환경변수의 PATH 항목을 살펴보면

4번째 항목과 5번째 항목이 서로 바뀐 것이 적용된 것을 확인할 수 있다

path_list = getSystemEnvironmentValue('PATH').split(';')
print(path_list[3] + '\n' + path_list[4])

>> 
C:\Python38\
C:\Python38\Scripts\

 

경로 추가, 삭제, 변경도 위와 같이 값 문자열을 리스트로 치환한 뒤, 리스트 요소 조작 후 다시 세미콜론 구분 문자열로 만들어 레지스트리 값을 쓰는 방식으로 구현하면 된다

 

경로를 추가/삭제하는 함수를 예로 작성해보자

def addSysEnvPath(path: str):
    origin = getSystemEnvironmentValue('PATH').split(';')
    origin.append(path)
    to_write = ';'.join(origin)
    setSystemEnvironmentValue('PATH', to_write)

def removeSysEnvPath(path: str):
    origin = getSystemEnvironmentValue('PATH').split(';')
    if path in origin:
        origin.remove(path)
    to_write = ';'.join(origin)
    setSystemEnvironmentValue('PATH', to_write)

 

4. 주의사항

레지스트리는 잘못 건드릴 경우 OS 동작에 치명적인 결함을 유발할 수 있으므로 사용함에 있어 각별한 주의가 요구된다

문제가 생겼을 경우 롤백할 수 있도록 레지스트리 값을 백업할 수 있게 구현해야 하며, 스크립트 및 함수는 엄격하게 제한된 인증을 거쳐서면 접근할 수 있도록 구현해야 한다

 

5. 기타

이 외에도 subprocess로 'SETX' 커맨드를 호출하기 혹은 배치 스크립트 작성 등 몇가지 방법들이 있는데, 그다지 pythonic하진 않기 때문에 관심있는 사람은 [참고]의 링크를 살펴보길 바란다

 

끝~!

 

[참고]

https://stackoverflow.com/questions/1085852/interface-for-modifying-windows-environment-variables-from-python

https://code.activestate.com/recipes/416087-persistent-environment-variables-on-windows/

반응형