상세 컨텐츠

본문 제목

Windows Console Command TREE 를 구현해보자

Development/C / C++

by Komastar.Dev 2014. 5. 27. 10:31

본문

반응형

2014. 05. 27. 10:00 작성

시작


Windows 7 64bit

Visual Studio Express 2013 for Windows Desktop 32bit


윈도우 명령창에서 동작하는 명령어인 tree 를 C++로 구현한다.

외관상 같은 결과를 보이도록 동작하게 만드는 것이 목표이다.

우선 tree의 출력 내용을 본다.

윈도우 명령창을 실행시키고 tree를 입력한다.

현재 경로의 하위 폴더를 모두 표시해준다.


옵션을 알아보기 위해 도움말을 호출해본다.

도움말을 보는 옵션은 /? 이며 윈도우 명령어들은 이 옵션으로 도움말을 볼 수 있다.

도움말에 옵션이 있으니 옵션을 사용해보도록 한다.

기본적으로 폴더만 보여주는데 /f 옵션을 추가하니 설명대로 파일도 출력된다.

/a 옵션을 보자

깔끔한 그래픽 특수문자 대신 텍스트 특수문자로 변경되었다.

혹시 이 두가지를 같이 사용해도 될까?

두가지 옵션이 모두 적용된 상태로 출력된다.

이번엔 도움말에서 본 경로를 지정해보자.

위와 같이 경로를 지정하니 해당 경로에서 tree 명령어를 실행한 결과가 나온다.

경로와 옵션 모두를 추가하면 하는대로 적용이 되어 출력된다.

옵션의 순서를 바꿔도 모두 적용이 된다.


- 출력되는 결과를 보고 정리를 하면 옵션의 종류는 2가지(F, A) 가 있고 경로를 지정하여 해당 경로의 하위 구조를 볼 수 있다.


- 옵션과 경로를 정상적으로 입력만 한다면 옵션과 경로의 위치는 섞여도 상관이 없으며 모두 적용되어 결과물이 나온다.


- 옵션이나 경로를 잘못 입력하였을 경우는 스크린샷 대신 텍스트로 대체하겠다

경로 정보가 두개 이상 들어가면 "매개 변수가 너무 많습니다."

경로 하위에 폴더나 파일이 없는 경우엔 "[현재 경로]에 하위 폴더가 없습니다."

옵션으로 입력한 글자가 잘못된 경우는 "잘못된 스위치 - [입력한 옵션]"

올바른 옵션에 글자를 더 입력하거나 중복 입력 혹은 3개 이상의 옵션인 경우엔 "매개 변수 형식이 틀립니다. - [입력한 옵션 중 마지막 옵션]"

위와 같이 예외 상황을 처리해준다.


이제 구현해야 하는 것들을 정리해보자.

기본적으로 현재 경로의 하위 구조를 출력해줘야 한다.

그래픽 문자는 까다롭기 때문에 일단 항상 /a 인 상태로 출력한다.

다른 경로를 지정하면 그 경로의 하위 구조를 출력한다.

옵션과 경로의 내용이 올바르다면 입력 순서가 바뀌는 것에 상관없이 모두 적용하여 출력한다.

2014. 05. 27. 10:31 임시 저장

우선 트리 형태로 표현하기 위한 방법을 생각해보자.

- 파일과 폴더로만 구분을 하고 폴더만 하위 파일이나 폴더를 가질 수 있다.

- 폴더의 하위 경로로 내려가는 경우 경로 표시 전에 공백을 둬서 하위임을 나타낸다.

- 같은 위상에 출력할 파일이나 폴더가 있다면 '+'문자로 현재 표시하는 대상이 Branch 임을 표시한다.

- 더 이상 표시할 파일이나 폴더가 없을 경우 마지막 폴더나 파일은 \로 표시해준다.

- 경로에 C:\ 를 입력하면 Root 기준으로 Tree를 그려준다.

- 경로에 C: 를 입력하면 현재 경로 기준으로 Tree를 그려준다.

- 경로 정보에 C:\ 경우 이외엔 \ 로 끝나지 않는다.

2014. 05. 28. 17:53 임시 저장


위의 조건을 만족하는 프로그램을 작성해야 한다.


프로그램에 필요한 기능을 정리를 한다면

파일을 탐색하는 함수를 알아야 하겠고, 하위 경로의 탐색을 위해선 재귀 호출을 사용하는 것이 가장 쉬워보인다.

탐색을 하고 난 뒤 이 파일이 폴더인지 아닌지 구분을 해야 한다.

옵션을 아무것도 입력하지 않은 상태로 실행한다면 실행하는 경로를 대상으로 탐색해야 하니 실행하는 경로를 알아야 한다.

기본 기능을 만드는 것이 우선이니 옵션은 위 기능들을 구현하고 생각해본다.


파일 탐색에 사용되는 함수를 알아본다.

구글에서 한글로 검색하는 것 만큼 답답한 일은 없다.

영어 문장을 완벽하게 작성해서 검색 할 필요도 없다. 우리가 알고 싶은건 파일을 찾는 것이니 단순하게 생각해서

find file 을 검색한다. 그러나 이렇게 검색하면 각종 언어의 내용이 다 나올 수 있으므로 in c를 붙이거나 c++을 붙인다.

검색 결과 Visual C++에서 사용하는 파일 탐색 함수는 findFirstFile 과 findNextFile 이다.

 

#include "stdafx.h"
#include 

#define ARGV_LENGTH 4   //  인자값의 최대 개수는 3개 (index 0번의 인자값에는 실행하는 경로가 들어가기 때문에)

using namespace std;

int PrintTree(LPCSTR cPath, int iRecursiveCount, int iFlag, unsigned int structure, int broCnt);
void PrintBranch(int iRecursiveCount, unsigned int uBranchData, int iFlag, int iBranchEnd);
void PrintErrorMsg(int iErrFlag, char* argv);
void PrintHelp(void);
int ExceptionHandle(int arCount);

int main(int argc, char* argv[])
{
    LPCSTR csPath = "";         //  탐색 경로를 저장 할 변수.
    int iErrCheckFlag = 0;      //  발생한 오류의 종류를 저장하는 변수.
    int iArgvLen[ARGV_LENGTH];  //  입력받은 인자값 각각의 길이.
    int iOption = 0;            //  입력받은 옵션에 따라 비트를 설정한다.
    int iErrCount = 0;          //  인자값 오류가 발생 할 경우 몇번째 값인지 확인.
    int iPathFlag = 0;          //  경로를 입력받았는지 확인.
    int iOptionFlagF = 0;       //  F옵션을 입력받았는지 확인.
    int iOptionFlagA = 0;       //  A옵션을 입력받았는지 확인.
    unsigned int uiTreeStructure = 0;   //  트리 구조의 줄기를 그릴 정보 저장.
    char cOpt;                  //  입력받은 옵션 치환.
    char sPathBuffer[MAX_PATH]; //  경로 임시 저장.
    
    DWORD dwSerial;             //  볼륨의 시리얼 번호를 저장.
    
    GetVolumeInformationA(LPSTR("C:\\"), NULL, NULL, &dwSerial, NULL, NULL, NULL, NULL);    //  현재 볼륨의 시리얼 번호를 가져옴.
    
    for (int i = 0; i < ARGV_LENGTH; i++)   //  각 인자값의 길이를 담을 배열을 초기화.
    {
        iArgvLen[i] = 0;
    }
    
    if (argc != 0)  //  인자값이 1개 이상이면 인자값의 개수가 정상인지 확인한다.
    {
        /* 옵션과 경로를 모두 입력해도 최대 3개의 인자만 받아진다.
        따라서 인자값의 개수가 3개를 초과하면 에러로 간주하고 처리.*/
        iErrCheckFlag = ExceptionHandle(argc);          //  인자값이 3개 이상이라면 오류 플래그에 값을 반환.
        PrintErrorMsg(iErrCheckFlag, argv[argc - 1]);   //  오류 플래그에 따라 오류의 종류를 출력하고 종료.
                                                        //  오류가 없다면 진행.
    }

    printf_s("폴더 PATH의 목록입니다.\n볼륨 일련 번호는 %04X-%04X입니다.\n\n", HIWORD(dwSerial), LOWORD(dwSerial));   // 현재 볼륨의 시리얼 번호 출력.

    for (int argCount = 1; argCount < argc; argCount++) //  입력된 인자값들을 구분하는 반복문 시작.
    {
        if (argv[argCount][0] == '/')       //  인자값이 옵션일 경우 이 분기로 들어간다.
        {
            if (argv[argCount][2] == NULL)  //  옵션은 f 혹은 a 한글자만 들어가기 때문에 2글자 이상이라면 오류로 처리한다.
            {
                cOpt = argv[argCount][1];   //  옵션 변수 치환.

                if (cOpt == 'f' || cOpt == 'F') //  대소문자 상관없이 옵션을 입력받을 수 있다.
                {
                    if (iOptionFlagF == 0)  //  F옵션이 처음 나온다면 옵션 플래그 비트를 설정한다.
                    {
                        iOption |= 0x1;
                        iOption &= 0x5;
                        iOptionFlagF = 1;
                    }
                    else                    //  F옵션이 두번이상 입력되면 오류로 처리한다.
                    {
                        iErrCheckFlag = 4;
                        iErrCount = argCount;
                    }
                }
                else if (cOpt == 'a' || cOpt == 'A')    //  마찬가지로 대소문자 구분 없이 입력받는다.
                {
                    if (iOptionFlagA == 0)  //  A옵션이 처음 나온다면 옵션 플래그 비트를 설정한다.
                    {
                        iOption |= 0x4;
                        iOption &= 0x5;
                        iOptionFlagA = 1;
                    }
                    else                    //  A옵션이 두번이상 입력되면 오류로 처리한다.
                    {
                        iErrCheckFlag = 4;
                        iErrCount = argCount;
                    }
                }
                else if (cOpt == 'h' || cOpt == 'H' || cOpt == '?') //  /h /H /? 옵션은 도움말을 출력한다.
                {
                    PrintHelp(); // 도움말 출력.
                    exit(1);    //  도움말을 출력했다면 프로그램 종료.
                }
            }
            else   //  옵션에 두글자 이상이 들어온 경우
            {
                iErrCount = argCount;   //  몇번째 인자에서 오류가 발생했는지 변수에 저장.
                iErrCheckFlag = 3;      //  오류의 종류를 변수에 저장.
            }
        }
        else      //   인자가 옵션이 아니라 경로 일 경우.
        {
            if (!iPathFlag) //  경로를 처음 입력 받는 경우
            {
                iPathFlag = 1;      //  더 이상 경로를 입력받지 않는다.
                strcpy_s(sPathBuffer, argv[argCount]);  //  입력받은 경로를 탐색 대상으로 한다.
            }
            else
            {
                iErrCheckFlag = 1;
                iErrCount = argCount;
            }
            strcat_s(sPathBuffer, "\\");
            csPath = sPathBuffer;
            printf("%s\n", csPath);
        }
    }
    PrintErrorMsg(iErrCheckFlag, argv[iErrCount]);  //  iErrCheckFlag에 발생한 오류의 종류가 저장되어있다. 오류에 맞게 메시지를 출력하고 종료.
    PrintTree(csPath, 0, iOption, uiTreeStructure, 1);  //  오류가 없다면 tree를 그리는 재귀함수를 호출한다.

	return 0;
}

int PrintTree(LPCSTR cPath, int iRecursiveCount, int iFlag, unsigned int uBranchData, int broCnt)
{
    WIN32_FIND_DATAA strFindData;   //  탐색한 내용을 저장할 구조체 선언
    HANDLE hHandle;                 //  탐색을 위해 핸들 선언

    int iFileType = 0;          //  폴더와 파일 구분
    int iBroNodeCount = 0;      //  형제 노드 개수 파악
    int iPrintCount = 0;        //  출력된 내용의 개수
    int iBranchEnd = 0;         //  가지의 끝
    char sCurrentPath[1000];    //  현재 경로 정보
    char sTempPath[1000];       //  경로 임시 저장
    string sBar = "";
    
    strncpy_s(sCurrentPath, cPath, strlen(cPath));              //  입력받은 경로 인자값을 현재 경로에 복사
    strncpy_s(sTempPath, sCurrentPath, strlen(sCurrentPath));   //  문자열 연산을 위해 임시 저장 변수에 복사
    strcat_s(sTempPath, "*");                                   //  모든 경로를 탐색하기 위해 *을 붙여줌
    
    hHandle = FindFirstFileA((LPCSTR)sTempPath, &strFindData);  //  경로 정보에 따른 파일을 탐색하고 구조체에 넣음
    
    if (hHandle == INVALID_HANDLE_VALUE)        //  핸들에 문제가 있으면 함수 종료
    {
        return 0;
    }

    /* counting brother node */
    if (broCnt > 0)
    {
        uBranchData |= (0x1 << (iRecursiveCount));      //  트리 구조를 그리기 위한 정보를 만든다.
    }

    do
    {
        if (!strcmp(strFindData.cFileName, ".") || !strcmp(strFindData.cFileName, ".."))
        { } //  현재 폴더와 상위 폴더를 뜻하는 '.'과 '..'은 제외한다.
        else
        {
            if (iFlag & 0x1)
            {
                iBroNodeCount++;    //  옵션 f 가 활성화되어 있다면 모든 노드 수를 센다.
            }
            else
            {
                iFileType = strFindData.dwFileAttributes;   //  탐색한 파일의 종류를 변수에 저장
                iFileType = iFileType >> 4;                 //  5번째 비트가 1이면 폴더
                if (iFileType & 0x1)
                {
                    iBroNodeCount++;    //  옵션 f가 비활성화 상태면 폴더 수만 센다.
                }
            }
        }
    } while (FindNextFileA(hHandle, &strFindData)); //  다음 파일을 탐색한다.
    
    FindClose(hHandle); // 형제 노드 탐색을 종료한다.

    /* print part */
    hHandle = FindFirstFileA((LPCSTR)sTempPath, &strFindData);

    do
    {
        if (!strcmp(strFindData.cFileName, ".") || !strcmp(strFindData.cFileName, ".."))
        {}
        else
        {
            iFileType = strFindData.dwFileAttributes;
            iFileType = iFileType >> 4;
            if (iFlag & 0x1)
            {
                iPrintCount++;  //  f 옵션이 활성화 상태면 매번 카운트를 증가시킨다.
            }
            else
            {
                if (iFileType & 0x1)
                {
                    iPrintCount++;  // f 옵션이 비활성화 상태면 폴더를 만날때만 카운트를 증가시킨다.
                }
            }

            if (iPrintCount == iBroNodeCount)   //  가지의 끝인지 확인한다.
            {
                iBranchEnd = 1;
                uBranchData &= ~(0x1 << iRecursiveCount);   //  가지가 끝났다면 이후로 가지를 출력하지 않는다.
            }
            else
            {
                iBranchEnd = 0;
            }

            if (!(iFlag & 0x1))
            {
                if (iFileType & 0x1)    //  f 옵션이 비활성화 상태이면 폴더만 출력한다.
                {
                    PrintBranch(iRecursiveCount, uBranchData, iFlag, iBranchEnd);
                    printf_s("%s\n", strFindData.cFileName);
                }
            }
            else    //  f 옵션이 활성화 상태면 모두 출력한다.
            {
                PrintBranch(iRecursiveCount, uBranchData, iFlag, iBranchEnd);
                printf_s("%s\n", strFindData.cFileName);
            }
            
            if (iFileType & 0x1)    //  폴더일 경우 하위 경로를 재귀호출
            {
                iRecursiveCount++;
                char nextPath[1000] = "";
                char sTempPath[1000] = "";
                strcat_s(nextPath, sCurrentPath);
                strcat_s(nextPath, (char*)strFindData.cFileName);
                strncpy_s(sTempPath, nextPath, strlen(nextPath));
                strcat_s(sTempPath, "\\");
                
                if (PrintTree(sTempPath, iRecursiveCount, iFlag, uBranchData, iBroNodeCount) == 1)
                {
                    iRecursiveCount--;
                }
            }
            else
            {
                continue;
            }
        }
    } while (FindNextFileA(hHandle, &strFindData)); //  다음 파일을 탐색

    FindClose(hHandle);
    
    return 1;
}

void PrintBranch(int iRecursiveCount, unsigned int uBranchData, int iFlag, int iBranchEnd)
{
    /* 트리의 가지를 그린다. 그래픽 문자와 텍스트 문자 중 택일 */
    for (int i = 0; i < iRecursiveCount; i++)
    {
        if (uBranchData & (0x1 << i))
        {
            if (iFlag & 0x4)
            {
                printf_s("|   ");
            }
            else
            {
                printf_s("\u2502   ");
            }
        }
        else
        {
            printf_s("    ");
        }
    }
    
    if (iFlag & 0x4)
    {
        // print normal ascii
        if (iBranchEnd)
        {
            printf_s("\\---");
        }
        else
        {
            printf_s("+---");
        }
    }
    else
    {
        // print exteneded ascii
        if (iBranchEnd)
        {
            printf_s("\u2514\u2500");
        }
        else
        {
            printf_s("\u251c\u2500");
        }
    }
}

int ExceptionHandle(int arCount)
{
    /* 인자 개수 오류를 처리 */
    if (arCount - 1 > 3)
    {
        return 4;
    }
    else
    {
        return 0;
    }
}

void PrintErrorMsg(int iErrFlag, char *argv)
{
    /* 오류 메시지 출력 */
    switch (iErrFlag)
    {
    case 1: 
        printf_s("매개 변수가 너무 많습니다 - %s\n", argv);
        exit(1);
        break;
    case 2: 
        printf_s("하위 폴더가 없습니다\n");
        exit(1);
        break;
    case 3: 
        printf_s("잘못된 스위치 - %s\n", argv);
        exit(1);
        break;
    case 4: 
        printf_s("매개 변수 형식이 틀립니다 - %s\n", argv);
        exit(1);
        break;
    default:
        break;
    }
}

void PrintHelp()
{
    /* 도움말 출력 */
    printf("\
드라이브 또는 경로의 폴더 구조를 그래픽으로 화면에 표시합니다.\n\n\
TREE [드라이브:][경로] [/F] [/A]\n\n\
   / F  각 폴더에 있는 파일 이름을 화면에 표시합니다.\n\
   / A  그래픽 문자대신 텍스트 문자를 사용합니다.\n\n");
}
반응형

관련글 더보기