코딩/C와 C++

4편, c++ 프로그래밍 CMake 예제

드리프트 2020. 12. 5. 16:21
728x170

 

 

안녕하세요? C++ 프로그래밍 4번째 편입니다.

 

3편까지 프로젝트의 기본 구성을 TDD (Test-Driven Development)에 맞게 구성했으며, 첫 번째 유닛 테스트까지 완료했습니다.

 

자 그럼, 원래의 목적인 renamer-youtube-dl에 맞게 라이브러리를 확장해 나가 볼까요?

 

그럼, TDD에 맞게 테스트를 해야 되는데 어떤 이유 때문에 우리가 이 프로그램을 만드는지 한번 볼까요?

 

https://cpro95.tistory.com/42

 

유튜브에서 블랙핑크 뮤직비디오 다운 받기

안녕하세요? 유튜브 보시다 보면 꼭 맘에 드는 콘텐츠를 다운로드하고 싶은 욕망이 생길 때가 있는데요. 그래서 나온 게 youtube-dl 이란 파이썬 프로그램이 있습니다. https://github.com/ytdl-org/youtube-dl

cpro95.tistory.com

상기 링크에서 보면 youtube-dl로 동영상이나 m4a 음원을 받으면 파일 이름에 꼭 이상한 문자가 해쉬태그처럼 붙어 나온다.

 

한곡만 다운로드하면 몰라도 유튜브 플레이리스트를 다운 받으면 그 수많은 파일 이름을 일일이 고치기 어렵다.

 

이럴 때 필요한 게 우리가 만들고자 하는 renamer-youtube-dl이다.

 

youtube-dl로 플레이리스트에서 m4a 음원만 다운받은 상태

위 스크린샷 보시면 youtube-dl 이 다운로드한 파일의 이름에 "-u7rKGj13pAs" 같은 해쉬태그가 붙어 있습니다.

 

이름 정리할 때 하나하나 지울 수 있는데 위 스크린샷처럼 파일이 많으면 일일이 수정하기 귀찮죠!

 

자, 이럴 때 필요하라고 우리가 만들 프로그램이 필요한 겁니다.

 

그럼, 우리가 필요한 기능이 먼저 원하는 파일이 뭐가 있는지 읽어 들이는 겁니다.

 

myStringLib 라이브러리에 첫 번째 Helper Function을 추가해 볼까요?

 

이름은 loadFiles이라고 합시다.

 

#include <string>
#include <dirent.h>

#include "spdlog/spdlog.h"

namespace myStringLib
{

    // lib test function
    int myStringLib(void)
    {
        spdlog::info("myStringLib Init...");
        return 0;
    }

    // loadFiles
    // get files with name m4a, mp4, avi at the given current path
    std::vector<std::string> loadFiles(const char *path)
    {
        std::vector<std::string> vMovieFiles;

        DIR *dirFile = opendir(path);
        if (dirFile)
        {
            struct dirent *hFile;
            while ((hFile = readdir(dirFile)) != NULL)
            {
                if (!strcmp(hFile->d_name, "."))
                    continue;
                if (!strcmp(hFile->d_name, ".."))
                    continue;

                if (hFile->d_name[0] == '.')
                    continue;

                if (strstr(hFile->d_name, ".m4a") || strstr(hFile->d_name, ".mp4") ||
                    strstr(hFile->d_name, ".avi"))
                {
                    std::string str(hFile->d_name);
                    vMovieFiles.push_back(str);
                }
            }
        }
        closedir(dirFile);

        return vMovieFiles;
    }
} // namespace myStringLib

 

loadFiles 함수를 간단히 설명해 보면,

 

경로를 인수로 받아서 그 경로에 있는 확장자 m4a, mp4, avi 파일의 이름을 std::string의 std::vector로 리턴합니다.

 

간단히 현재 폴더에서 음원이나 동영상 파일의 이름을 갖는 vector를 리턴하는 거죠.

 

GCC 컴파일러나 Clang 컴파일러를 쓰기 때문에 dirent.h 파일을 include 해야 됩니다.

 

이제 loadFiles를 이용해 얻은 파일에서 확장자를 뺀 나머지 부분만 추출하는 fucntion을 만들어 볼까요?

 

namespace myStringLib
{
/*
....
....
....
*/

    	// getFileName
	// get the name of file without extension
	std::string getFileName(std::string str)
	{
		unsigned found = str.find_last_of(".");
		return str.substr(0, found);
	}
    
} // namespace myStringLib

 

함수의 이름은 getFileName입니다.

 

코드는 std::string 하나를 불러오면 즉, 파일 이름을 넣어주면 "."을 포함해서 확장자를 제외한 이름만 std::string으로 리턴합니다.

 

예를 들어 함수 인자가 "test.m4a" 라면 "test"만 리턴해 주는 방식입니다.

 

 

 

 

 

이제 본격적인 라이브러리 함수를 만들었으니까 TDD 방식에 따라 유닛 테스트 코드를 만들어 볼까요?

 

test의 myTest.cpp 에서 아래와 같이 유닛 테스트 코드를 추가합시다.

 

#define CATCH_CONFIG_MAIN
#include <string>

#include "../src/lib/myStringLib.hpp"
#include "catch.hpp"
#include "spdlog/spdlog.h"

TEST_CASE("myStringLib")
{
  SECTION("Constructor")
  {
    spdlog::info("myStringLib Consturctor");
    REQUIRE(myStringLib::myStringLib() == 0);
  }

  SECTION("getFileName")
  {
    spdlog::info("getFileName : test.m4a");
    REQUIRE(myStringLib::getFileName(std::string{"test.m4a"})
                .compare(std::string{"test"}) == 0);
  }
}

 

SECTION을 "getFileName"으로 하나 만들고

 

REQUIRE 함수를 넣어서 잘 작동하는지 테스트하는 겁니다.

 

REQUIRE의 함수 인자를 잘 보시면

 

myStringLib::getFileName(std::string {"test.m4"})입니다.

 

이걸 실행하면 "test"만 리턴해야 되는 겁니다.

 

그래서. compare(std::string {"test"}) == 0으로 코드를 짰습니다.

 

위에서 "test"를 compare 했을 때 둘이 같다면 0을 나타내기 때문에 테스트를 통과하는 방식이죠.

 

그럼 테스트를 한번 해볼까요?

 

 

100% tests passed라고 나오네요.

 

그럼 일부러 에러가 나게 한번 해볼까요?

 

  SECTION("getFileName")
  {
    spdlog::info("getFileName : test.m4a");
    REQUIRE(myStringLib::getFileName(std::string{"test.m4a"})
                .compare(std::string{"test."}) == 0);
  }

 

위 코드처럼 compare 인자에 "test."라고 "."을 하나 추가했습니다.

 

다시 유닛 테스트해볼까요?

 

 

예상데로 "Failed"가 나왔습니다.

 

그럼 원래대로 코드를 바꿔놓고 다른 기능을 함수에 추가해 봅시다.

 

파일 이름을 불러와서 확장자를 뺀 이름만 불러왔기 때문에 여기서 우리가 원하지 않는 해쉬태그 부분만 없애는 함수를 만들어 볼까요?

 

myStringLib.h 파일에 다음 코드를 추가합시다.

 

    // substrHyphen
    // remove Hyphen return remains
    std::string substrHyphen(std::string str)
    {
        std::string testData = myStringLib::getFileName(str);
        if (testData.length() < 13)
            return testData;
        else
            return testData.substr(0, testData.length() - 12);
    }

 

우리의 목적은 해쉬태그를 없애는 건데 해쉬태그는 무조건 "-" 하이픈 문자부터 시작해서 12 글자입니다.

std::string의 substr을 썼습니다.

 

substr(0, testData.length() - 12); 여기서 12는 youtube-dl로 다운로드하였을 때 해쉬태그 문자 숫자입니다.

 

예를 들어 "You Never Know-4Kk_iaaHd_Y.m4a"라고 다운받을을때 "-" 시작해서 "."까지 총 12글자입니다.

 

나중에 youtube-dl 이 수정돼서 13글자가 나오면 위 코드를 13으로 바꾸면 됩니다.

 

그럼 이제 다시 테스트해볼까요?

 

  SECTION("substrHyphen")
  {
    spdlog::info("givenData : test-12345678901.m4a");
    std::string givenData{"test-12345678901.m4a"};
    spdlog::info("{}", myStringLib::substrHyphen(givenData));
    REQUIRE(myStringLib::substrHyphen(givenData).compare(std::string{"test"}) ==
            0);
  }

 

코드는 간단합니다. "test-12345678901.m4a" 즉 "-"로 시작해서 12글자가 해쉬태그인데

 

myStringLib::substrHyphen 함수를 돌리면 "test"만 리턴되는지 확인하는 방식입니다.

 

아래는 유닛 테스트 결과입니다.

 

 

잘 테스트됐네요.

 

그럼 substrHyphen 함수에서 12를 13으로 바꾸면 어떻게 되는지 볼까요?

 

    // substrHyphen
    // remove Hyphen return remains
    std::string substrHyphen(std::string str)
    {
        std::string testData = myStringLib::getFileName(str);
        if (testData.length() < 14)
            return testData;
        else
            return testData.substr(0, testData.length() - 13);
    }

 

위와 같이 고치면 에러나 나야 하는 유닛 테스트 결과입니다.

 

 

결과는 예상처럼 "Failed"입니다.

 

그럼 원래대로 12로 바꾸고 다시 우리의 라이브러리를 완성해 봅시다.

 

이제 본격적으로 우리가 원하는 파일 이름 바꾸는 함수를 추가해 봅시다.

 

	// getExtension
    // get the ext of file
    std::string getExtension(std::string str)
    {
        unsigned found = str.find_last_of(".");
        return str.substr(found);
    }

    // renameFiles
    // wrapper of cpp version of c std rename library
    int renameFiles(std::string oldname, std::string newname)
    {
        int result = rename(oldname.c_str(), newname.c_str());

        if (result != 0)
        {
            perror("Error renaming file");
            return 1;
        }
        else
            return 0;
    }

 

함수 두 개를 추가했는데 한 개는 확장자만 리턴하는 함수고

 

다른 한개는 파일 이름을 바꾸는 함수입니다.

 

코드 구성이야 구글링 하면 코드 예제로 많이 나오는 겁니다. 따로 설명이 필요 없을 거 같네요.

 

그럼 본격적인 UI를 만들어야 하는데 GUI로 만들면 너무 시간이 많이 걸리니까 CUI 즉 콘솔에서 하는 UI 만들어 봅시다.

 

이제 본격적인 main.cpp 파일을 고쳐보겠습니다.

 

#include <iostream>
#include <string>

#include "spdlog/spdlog.h"
#include "lib/myStringLib.hpp"

void print_vector(std::vector<std::string> &vMovieFiles)
{
    spdlog::info("Make Table...");
    for (auto val : vMovieFiles)
    {
        std::cout << val << "\n";
    }
}

int main(int argc, char **argv)
{

    spdlog::info("youtube-dl name fix it");
    myStringLib::myStringLib();

    std::vector<std::string> vMovieFiles;

    spdlog::info("Loading files...");
    vMovieFiles = myStringLib::loadFiles(".");
    if (vMovieFiles.size() == 0)
    {
        spdlog::info("Loading files...Failed : No Files");
        exit(1);
    }
    spdlog::info("Loading files...Done");

    std::cout << "\n";
    print_vector(vMovieFiles);
}

 

main 함수는 간단합니다.

 

현재 디렉터리에서 loadFiles(".")하고 vMovieFiles에 파일 이름을 넣고 프린트하는 루틴입니다.

 

현재 디렉터리에 있는 모든 파일을 print_vector() 함수에서 출력해주는 단순한 코드입니다.

 

다음 코드는 UI 부분입니다.

 

char c;
    std::cout << "Fix file names? (y/n) ";
    std::cin >> c;
    if (c == 'y' || c == 'Y')
    {
        spdlog::info("rename it");
        for (auto val : vMovieFiles)
        {
            // NOT alreadyRenamed!!!
            if (!myStringLib::alreadyRenamed(val))
            {
                int ret =
                    myStringLib::renameFiles(val, myStringLib::substrHyphen(val) +
                                                      myStringLib::getExtension(val));
                if (ret == 0)
                    spdlog::info("{} is renamed successful!", val);
                else
                    spdlog::info("{} has error when renaming", val);
            }
        }
    }
    else
    {
        spdlog::info("not rename it");
        exit(1);
    }

 

파일 이름을 고칠까요? 물어보고 "Y"라고 답하면 고치는 루틴입니다.

 

여기에 myStringLib::alreadyRenamed() 함수가 있는데 간혹 파일 이름을 고친 후에 또 실행할 경우를 대비해서 넣었습니다.

 

myStringLib.h의 alreadyRenamed() 코드는 다음과 같습니다.

 

    // alreadyRenamed
    // check if there is "-", return true or false
    bool alreadyRenamed(std::string str)
    {
        std::string testData = myStringLib::getFileName(str);
        if (testData.length() < 13)
            return true;
        if (testData.substr(testData.length() - 13 + 1, 1)
                .compare(std::string{"-"}) == 0)
        {
            return false;
        }
        else
        {
            return true;
        }
    }

 

일단은 글자 수 및 "-"유무에 따라 true false를 리턴하는 코드입니다.

 

그럼 alreadyRenamed()의 유닛 테스트를 만들어 볼까요?

 

  SECTION("alreadyRenamed")
  {
    REQUIRE(
        !myStringLib::alreadyRenamed(std::string{"test-12345678901.m4a"}));
    REQUIRE(myStringLib::alreadyRenamed(std::string{"test.m4a"}));
  }

 

첫 번째 REQUIRE는 flase가 나와서 통과하고

 

두 번째 REQUIRE는 truer가 나와야 통과하는 루틴입니다.

 

테스트 결과는?

 

 

통과됐네요. ㅎㅎ

 

이제 우리의 목적에 맞는 C++ 프로그램이 완성되었습니다.

 

UI 부분은 QT나 기타 GUI 코드로 추가하시면 멋진 프로그램이 될 겁니다.

 

중요한 건 GUI 가 아니라 로직이 중요하다고 생각합니다.

 

그럼. 이것으로 c++ 프로그래밍 튜토리얼을 마치겠습니다.

그리드형