엑셀 VBA에서 dir, ipconfig, ping 같은 명령의 결과를 문자열로 받아야 할 때가 있습니다. 가장 쉬운 방법은 WScript.Shell의 Exec입니다. .StdOut.ReadAll로 출력이 바로 들어오니까요. 그런데 실행할 때마다 검은 cmd 창이 잠깐 떴다가 사라집니다. 혼자 쓰는 매크로면 넘어가겠지만, 다른 사람에게 배포하는 파일에서 창이 번쩍이면 "이거 바이러스 아니냐"는 소리를 듣기 딱 좋습니다.

Exec는 왜 창을 숨길 수 없나

Exec에는 창 상태를 지정하는 인자가 없습니다. 반면 Run은 두 번째 인자로 창 스타일을 받고, 0을 주면 창을 띄우지 않습니다. 대신 Run은 출력을 돌려주지 않으니, 명령 결과를 임시 파일로 리다이렉트(>)한 다음 그 파일을 읽는 우회로를 씁니다.

광고

아래는 특정 폴더 아래의 엑셀 파일을 하위 폴더까지 전부 찾아서 목록을 메시지 박스로 보여주는 예제입니다.

Sub 파일검색()

    Dim dirPath As String, temPath As String, T As String

    dirPath = """D:\My Project\Excel\*.xls*"""
    temPath = "D:\tem.txt"

    CreateObject("wscript.shell").Run "cmd /c dir " & _
        dirPath & " /s /b > " & temPath, 0, False

    Do Until Dir(temPath) <> "": DoEvents: Loop
    Do Until FileLen(temPath) > 0: DoEvents: Loop

    With CreateObject("Scripting.FileSystemObject")
        T = .OpenTextFile(temPath).ReadAll()
        On Error Resume Next
        Do
            If Err Then Err.Clear
            .DeleteFile temPath
        Loop Until Err = 0
    End With

    MsgBox T

End Sub

몇 군데 짚어 보면 이렇습니다.

  • dirPath에 따옴표를 세 겹으로 쓴 건 경로의 My Project에 공백이 있어서입니다. VBA 문자열 안에서 ""가 따옴표 한 개가 되니, 결과적으로 cmd에는 "D:\My Project\Excel\*.xls*"처럼 따옴표로 감싼 경로가 넘어갑니다.
  • /s는 하위 폴더까지, /b는 날짜·크기 없이 전체 경로만 한 줄씩 출력하는 옵션입니다.
  • Run의 세 번째 인자 False는 명령이 끝날 때까지 기다리지 않는다는 뜻입니다. 그래서 바로 아래 두 개의 Do Until 루프가 파일이 생길 때까지, 그리고 내용이 쓰일 때까지 기다립니다.
  • 다 읽은 뒤에는 임시 파일을 지우는데, cmd가 아직 파일을 잡고 있으면 삭제가 실패하므로 에러가 안 날 때까지 반복합니다.

실제로 돌려 보면 걸리는 부분이 하나 있습니다. 검색 결과가 하나도 없으면 dir이 표준 출력에 아무것도 안 쓰고(오류 메시지는 표준 오류로 갑니다), 파일 크기가 계속 0이라 두 번째 루프가 끝나지 않습니다. 결과가 없을 수 있는 명령이라면 세 번째 인자를 True로 줘서 명령이 끝날 때까지 기다리게 하고 대기 루프를 빼는 쪽이 안전합니다. 또 임시 파일 경로가 D:\로 고정돼 있는데, D 드라이브가 없거나 쓰기 권한이 없는 PC도 있으니 배포용이라면 Environ("TEMP") 아래로 바꿔 두는 걸 권합니다.

Exec로 하면 코드는 짧아집니다

창이 떠도 상관없다면 Exec가 훨씬 간단합니다.

    Dim T As String, RunCommand As String
    RunCommand = "cmd /c dir " & """D:\My Project\Excel\*.xls*"""
    T = CreateObject("Wscript.Shell").Exec( _
            RunCommand _
        ).StdOut.ReadAll

이 코드는 /s /b 옵션을 넣지 않았기 때문에 위 예제와 결과 모양이 다릅니다. 해당 폴더만 검색하고, 볼륨 정보와 날짜·크기·요약 줄이 붙은 일반 dir 출력이 그대로 들어옵니다. 그리고 앞서 말한 대로 실행 순간 cmd 창이 보입니다.

ipconfig처럼 결과를 직접 확인해 보고 싶을 때는 직접 실행 창(Ctrl+G)에서 이렇게 돌려 보면 됩니다. Debug.?는 입력하면 VBE가 Debug.Print로 바꿔 줍니다.

    Debug.? CreateObject("wscript.shell") _
        .Exec("ipconfig /all") _
        .StdOut.ReadAll()

VBA 직접 실행 창에 ipconfig /all 결과가 출력된 화면

이렇게 내 PC의 네트워크 설정을 빠르게 훑어볼 때 쓸 만합니다.

IP 주소만 필요하다면 WMI가 낫습니다

내 PC의 IP를 얻으려고 ipconfig 출력을 파싱하는 건 번거롭고, 윈도우 언어에 따라 항목 이름도 달라집니다. 이럴 땐 cmd를 거치지 않고 WMI에 직접 물어보는 게 깔끔합니다. 창도 안 뜹니다.

Function GetIPAddress()
    Const strComputer As String = "."   ' 컴퓨터 이름. 점(.)은 로컬 컴퓨터
    Dim objWMIService, IPConfigSet, IPConfig, IPAddress, i
    Dim strIPAddress As String

    ' WMI 서비스에 연결
    Set objWMIService = GetObject("winmgmts:" _
        & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

    ' TCP/IP가 활성화된 네트워크 어댑터 전체 조회
    Set IPConfigSet = objWMIService.ExecQuery _
        ("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled=TRUE")

    ' 어댑터마다 연결된 IP 주소를 모두 이어 붙임
    For Each IPConfig In IPConfigSet
        IPAddress = IPConfig.IPAddress
        If Not IsNull(IPAddress) Then
            strIPAddress = strIPAddress & Join(IPAddress, ", ")
        End If
    Next

    GetIPAddress = strIPAddress
End Function

Win32_NetworkAdapterConfiguration에서 IPEnabled=TRUE인 어댑터만 골라 IPAddress 배열을 Join으로 이어 붙입니다. 이 배열에는 IPv4와 IPv6 주소가 같이 들어 있어서 결과가 192.168.x.x, fe80::... 같은 모양이 됩니다. IPv4만 쓰려면 배열 첫 번째 요소만 꺼내거나 :가 없는 값만 고르면 됩니다. 어댑터가 여러 개(유선, 무선, 가상 어댑터 등)면 구분자 없이 바로 이어 붙여지니, 어댑터 사이에도 쉼표를 넣도록 손봐 두면 결과를 나누기 편합니다.