[C] 프로그램 파라미터를 처리하는 getopt() 사용법

C프로그램의 파라미터 파싱 및 처리

문자열 파싱은 스크래치 상태(아무것도 없는 빈 상태)로부터 구현하기가 만만치 않은데, 이를 도와주는 라이브러리 함수가 있으니 바로 unistd.h에 정의된 getopt() 함수이다.

getopt()

이 함수의 원형은 다음과 같다.

int getopt(int argc, char * const argv[], const char *optstring);

이 함수의 파라미터는 간단한다.

  1. argc, argv : main() 함수가 받은 파라미터를 그대로 전달한다.
  2. optstring : 파싱해야 할 파라미터를 쓴다. 옵션이 별도의 파라미터를 받는 경우 콜론을 함께 쓴다.

예를 들어 -h, -v, -f filename을 받는 세 가지 옵션이 있다고 하면 옵션스트링은 "hvf:"가 된다. 각각의 옵션을 파싱해내기 위해서는 getopt()함수가 0을 리턴할 때까지 계속해서 반복하면 된다.

그외 전역변수

이 함수와 관련된 전역 변수에는 다음과 같은 것들이 있다.

  • optarg : 옵션 뒤에 별도의 파라미터 값이 오는 경우, 이를 파싱한 결과 파라미터 값은 optarg에 문자열로 저장된다.
  • optind : 다음번 처리될 옵션의 인덱스이다. 만약 파싱한 옵션이후에 추가적인 파라미터를 받는다면 (예를 들어 입력 파일 이름 같이) 이 값을 활용할 수 있다. getopt()함수는 한 번 호출될 때마다 이 값을 업데이트한다.
  • opterr : 옵션에 문제가 있을 때, 이 값은 0이 아닌 값이되며, getopt()함수가 메시지를 표시하게 된다.
  • optopt : 알 수 없는 옵션을 만났을 때 해당 옵션이 여기에 들어간다. (이 때 getopt의 리턴값은 ‘?’가 된다.)

샘플 코드

아래 샘플 코드는 간략한 예를 보여준다. 이 코드에서는 a, b, c의 세 옵션을 인식하며 각각의 옵션이 주어지는 경우 해당 플래그 변수를 1로 정의하고 그 결과를 출력한다.

1:  #include <stdio.h>
2:  #include <unistd.h> // for getopt()
3:  
4:  int main(int argc, char * const * argv){
5:      int flag_a = 0, flag_b = 0, flag_c = 0;
6:      int c; // option
7:      while( (c = getopt(argc, argv, "abc")) != -1) {
8:          // -1 means getopt() parse all options
9:          switch(c) {
10:             case 'a':
11:                 flag_a=1;
12:                 break;
13:             case 'b':
14:                 flag_b=1;
15:                 break;
16:             case 'c':
17:                 flag_c=1;
18:                 break;
19:             case '?':
20:                 printf("Unknown flag : %c", optopt);
21:                 break;
22:         }
23:     }
24:     if(flag_a) {printf("flag a is ON \n");}
25:     if(flag_b) {printf("flag b is ON \n");}
26:     if(flag_c) {printf("flag c is ON \n");}
27:     return 0;
28: }

보통 while문으로 루프를 돌면서 옵션들을 하나씩 검사한다. getopt가 optstring으로 정의되지 않은 옵션문자를 만나면 opterr 값이 세팅되고, 동시에 getopt()함수가 해당 문자에 대해 메시지를 출력한다. 이 때 반환값은 '?'가 되고 이때 알 수 없는 옵션 문자는 optopt에 저장된다.

추가 파라미터를 필요로하는 옵션

gcc의 -o와 같이 파일 이름등의 추가 파라미터를 필요로하는 옵션은 다음과 같이 처리한다.

  1. optsting에서 옵션 문자 뒤에 콜론을 붙인다. "abcf:"와 같은 식으로 쓴다.
  2. getopt()함수는 -f를 만나면 자동으로 그 뒤에 붙은 문자열을 공백까지 탐색하여 이를 optarg에 복사한다.
  3. 처리시에는 리턴값이 ‘f’인 경우 optarg의 값을 그대로 액세스하면 된다.

이러한 처리 코드는 아래와 같다.

case 'f':
    memcpy(file_name, optarg, strlen(optarg));
    break;

또한 파라미터가 있다고 정의한 경우, 파라미터 값이 빠진채로 실행되었다면 이 때는 옵션이 인식할 수 없는 옵션으로 처리된다.

case '?':
    if(optopt == 'f') {
        printf("option -f requires FILENAME\n");
    }
    break;

optind

옵션문자들을 모두 처리한 이후에 프로그램 자체의 파라미터를 처리하기 위해서는 optindargv의 인덱스로 사용하여 파라미터를 전달 받을 수 있다.

그외 긴 이름

--output등의 긴 이름 옵션을 사용하기 위해서는 getoptlong함수를 사용한다.

[C] 동적으로 할당한 메모리 영역의 크기를 나중에 알 수 있게 하기

동적으로 할당받은 메모리의 크기를 구하기

sizeof 함수는 (정확히는 매크로 함수) 타입의 크기를 반환한다. 배열 이름은 배열의 시작번지를 나타내므로 포인터와 거의 동일하게 취급되고, 또 많은 서적에서 이 둘을 같은 것이라고 말하는데, 사실은 다르다. sizeof() 함수는 배열을 인자로 넣으면 배열의 크기를 알려준다. 따라서 배열의 요소의 개수는 sizeof(intArr) / sizeof(int)와 같은 식으로 구할 수 있다.

이와 반대로 동적으로 메모리를 할당받는 malloc 류의 함수는 포인터를 반환한다. 포인터는 unsigned long int형으로 시스템에 따라 4혹은 8바이트의 크기가 된다. 따라서 배열을 동적으로 할당하기 위해 메모리 할당 함수를 사용하면 sizeof(포인터이름)은 포인터의 크기를 반환하므로 배열의 요소의 개수를 알아낼 수가 없다.

사실 이미 메모리를 할당하는 시점에, 크기를 지정해 주었으므로 그 영역의 크기는 알고 있는 상황이지만 이 값을 변수에 담아서 계속 들고다니기란 조금 귀찮다. 여기서는 배열을 할당할 시에 그 크기를 나중에 알 수 있도록 하는 꼼수를 소개한다. [C] 동적으로 할당한 메모리 영역의 크기를 나중에 알 수 있게 하기 더보기

[c/c++] SMI를 SRT로 컨버팅하기

윈도버전

컴파일 시 큰 문제는 없는데, 대입하는 포인터의 형이 달라서 경고가 뜨는 경우가 있다는데, 내 경우에는 윈도에서 컴파일시 오류가 났다. char* 형에대해 메모리를 할당할 때 캐스팅을하지 않으면 void*로 취급하므로 오류가 나면서 컴파일이 안됐다. 수정~ [c/c++] SMI를 SRT로 컨버팅하기 더보기

[C/C++] 구조체로 구성된 애플리케이션 정보를 디스크에 기록하는 법 2

[이 글은 혼자서 연구하는 C/C++의 예제의 내용을 사용하고 있습니다.]

지난 글에서는 구조체로 구성되는 어떤 정보들을 디스크에 기록하는 방법을 예시를 통해 살펴보았다.  하지만 지난 예제에서는 단지 기록과 읽어들이는 작업에 초점을 맞추면서 아주 단순한 구조체가 레코드를 이루고 있었고, 저장이나 복원에 관해서는 단지 구조체 배열을 그대로 쓰고 읽는 수준이었다.

이번에는 구조체내에서 포인터를 다루는 경우를 반영해 예제를 새로 작성한다. 포인터 자체는 메모리의 번지값이므로 디스크에 기록하는 것이 무의미하며, 그 포인터가 가리키는 실제 값을 저장하는 것이 중요하다. (물론 이는  &연산자를 빼고 쓰면 간단하다) 문제는 기록시에 얼마만큼의 양을 기록할 것인지 여부이다. 만약 이전 예제에서 친구의 이름이 실행 시에 입력을 받게 되고, 따라서 가변적이라면 어떻게 처리할 수 있을까.

파일에 정보를 기록할 때는 복원에 필요한 정보를 모두 기록해주는 것이 맞다. 따라서 이 경우에는 기록할 문자열의 길이를 구해서 함께 저장해주고, 이를 읽어 문자열을 얼마나 읽어야 할지에 대해 알 수 있도록 해야 한다. 또한 구조체 내에서 문자열에 대한 포인터만 가지고 있으므로 문자열을 입력받을 때는 매번 메모리를 할당해야 하며, 반대로 메모리를 해제하는 작업도 잊지 않고 해주어야 한다.

#include <stdio.h>
#include <string.h>
#define MYFILENAME "/Users/sooop/temp/friends.dat"
void writeFriends();
void readFreinds();
void deleteAll();

typedef struct __frinend{
    char *name;
    int age;
    double height;
} friend;

struct __header{
    char desc[100];
    int ver;
    int count;
};

friend friends[32];
int num = 5;

int main(void)
{
    int i, sel;

    // default data
    friends[0].name = (char*)malloc(32);
    strcpy(friends[0].name, "Lee A");
    friends[0].age = 23;
    friends[0].height = 180.1;
    friends[1].name = (char*)malloc(32);
    strcpy(friends[1].name, "Lee B");
    friends[1].age = 23;
    friends[1].height = 182.1;
    friends[2].name = (char*)malloc(32);
    strcpy(friends[2].name, "Lee C");
    friends[2].age = 23;
    friends[2].height = 184.1;
    friends[3].name = (char*)malloc(32);
    strcpy(friends[3].name, "Lee D");
    friends[3].age = 23;
    friends[3].height = 188.1;
    friends[4].name = (char*)malloc(32);
    strcpy(friends[4].name, "Lee E");
    friends[4].age = 23;
    friends[4].height = 180.1;

    for(;;)
    {
        printf("\n\n\n\n1.Show 2.Save 3.Del 4.Read 9.Quit : ");
        sel = getchar();
        switch(sel){
            case '1':
                for(i=0;i<num;i++)
                {
                    printf("\n");
                    printf("Name:%s,\tAge:%d,\tHeight:%.1f\n",friends[i].name, friends[i].age, friends[i].height);
                }

                printf("\nCompleted.\n");
                break;
            case '2':
                writeFriends();
                break;
            case '3':
                deleteAll();
                break;
            case '4':
                readFreinds();
                break;
            case '9':
                deleteAll();
                return 0;
        }
    }           

    deleteAll();
    return 0;
}

void deleteAll()
{
    int i;
    for(i=0;i<num;i++)
    {
        if(friends[i].name != NULL){
            free(friends[i].name);
        }
    }
    memset(friends, 0, sizeof(friends));
    num = 0;

    printf("\nCleared.\n");
}

void writeFriends()
{
    struct __header h;
    int len;
    int i;

    FILE *f = fopen(MYFILENAME,"wb");
    /* write header*/
    strcpy(h.desc,"Friends");
    h.count = num;
    h.ver = 110;

    fwrite(&h, sizeof(struct __header), 1, f);

    for (i=0;i<num;i++)
    {
        len = strlen(friends[i].name);
        fwrite(&len,sizeof(int),1,f);
        fwrite(friends[i].name, len, 1, f);
        fwrite(&friends[i].age, sizeof(int), 1, f);
        fwrite(&friends[i].height, sizeof(double), 1, f);
    }

    printf("\nSaved.\n");
    if(f) fclose(f);
}

void readFreinds()
{
    int i, len;
    struct __header h;

    FILE *f = fopen(MYFILENAME,"rb");

    if(f == NULL){
        printf("\nCan't Open File.\n");
        return;
    }
    // reading header
    deleteAll();
    fread(&h, sizeof(struct __header), 1, f);
    num = h.count;

    for(i=0;i<num;i++)
    {
        fread(&len,sizeof(int),1,f);
//        printf("%d length\n", len);
        friends[i].name = (char*)calloc(1,len+1);
        fread(friends[i].name, len, 1, f);
        fread(&friends[i].age, sizeof(int), 1, f);
        fread(&friends[i].height, sizeof(double), 1, f);
    }

    printf("\n\n%d %s loaded.\n", num, (num>1)?"records":"record");

    if(f) fclose(f);
}

저장하는 부분

헤더를 저장하는 부분은 이전 버전과 별 다를 게 없다. 대신 문자열의 길이를 저장하기 위해 len이라는 변수를 하나 더 사용한다. 결국 저장되는 데이터의 구조는 다음과 같이 될 것이다.

[설명][버전][개수]
[길이][이름값][나이값][신장값]
[길이][이름값][나이값][신장값]

읽어오는 부분

읽어오는 부분은 조금 더 복잡해졌다. 헤더 부분까지를 먼저 읽어서 저장된 레코드의 개수가 몇 개인지를 먼저 파악한다. 여기까지는 지난 번과 동일하다. 이전 버전은 모든 레코드의 크기가 동일했기 때문에 그냥 구조체 배열 전체를 읽어와버리면 됐는데, 이번에는 하나 하나 읽어와서 메모리에 차곡차곡 넣어준다는 점이 다르다. 또한 구조체에는 문자열의 주소 정보만을 담고 있으니, 문자열을 로드하기 위해서는 메모리를 할당받아야 한다는 점도 잊지 말자. 메모리를 할당할 때는 문자열의 끝을 알리는 널문자를 위한 공간을 위해 1바이트를 더 할당해주어야 한다.

1. len에 이름 문자열의 길이를 읽어와 담는다.
2. name에 저장될 실제 문자열의 길이를 알았으므로 메모리의 임의의 영역을 할당받아둔다. 이때 할당 받는 크기는 len보다 1커야 한다. 널문자까지 포함해야 하기 때문이디다.
3. 그런 다음 파일로부터 문자열을 len만큼 읽어온다. 나이와 신장에 대한 정보는 같은 방법으로 (단 읽어와야 하는 바이트 수는 고정되어 있다) 읽어오면 된다.

지우는 부분

지우는 부분 역시 이름을 포인터로 잡으면서 발생한 부수적인 추가 작업이다. 레코드를 삭제할 때는 해당 레코드에서 이름 값의 메모리를 해제하고, 해당 레코드가 점유하고 있던 구조체 요소를 0으로 채운다. 이전 예제에서는 구조체 배열 전체의 크기만큼 0으로 채웠으나, 이 번에는 할당해준 메모리를 해제하는 작업만이 추가되었다고 보면 간단하다.

그외에 예외 등으로 프로그램을 종료하기 이전에는 반드시 malloc, calloc 등으로 할당받은 메모리 영역들을 해제해야 한다. 그래서 메모리 해제 부분을 포함하여 삭제 부분을 따로 함수로 만들었다.

[C/C++] 구조체로 구성된 애플리케이션 정보를 디스크에 기록하는 법 1

이번 글은 구조체를 파일로 저장하고 읽어들이는 예제. 구성 원리는 간단하다. 파일 스트림을 사용해서 데이터를 읽고 쓴다. 여기에는 fwrite, fread라는 두 함수를 사용하는데 두 함수로 전달되는 인자는 동일하고, 반대의 작용을 할 뿐 원리는 동일하다.

fwrite : 특정 메모리 주소에서 시작하여 정해진 길이만큼, 또 그 개수만큼을 지정된 파일 스트림에 기록한다.

fwrite(&myVar, sizeof(myVar), 1, f)

혹은

fwrite(arrMyFriends, sizeof(elMyFriend), count, f)

와 같은 방식이다.

읽어올 때 사용하는 함수인 fread의 경우도 마찬가지인데, 특정한 길이만큼 메모리의 정해준 번지로 읽어들인다.

파일 스트림을 사용할 때는 연속적으로 읽고 쓰게 된다. 따라서 파일에는 그냥 무작정 기록만 할 것이 아니라 헤더 정보를 담는 구조체를 함께 저장하여 몇 개의 데이터가 있는지, 그리고 프로그램 버전에 따라 혹시 저장 포맷이 바뀔 수도 있으니 버전은 어떤지 등등을 체크해주는 부분을 넣으면 되겠다.

#include <stdio.h>
#include <string.h> //for memset(), strcpy()

void writeFriend();
void readFriend();

struct __friend {
	char name[20];
	int age;
	double height;
};

struct __friend friends[256] = 
{
	{"Lee Kiyoung", 30, 172.2f},
	{"Kim Taeyoung", 19, 182.5f},
	{"Choi Soonyeol", 25, 183.4f},
	{"Roh Minho", 38, 154.5f},
	{"", 0, 0},
};

struct __header {
	char desc[50];
	int ver;
	int count;
};

int num = 4;

int main(void)
{
	int sel, i;

	for(;;)
	{
		printf("1.view 2.save 3.delete 4.read 9.quit  ");
		sel = getchar();

		switch(sel)
		{
			case '1':
				for (i=0;i<num;i++) {
					printf("Name:%s, Age:%d, Height:%.1f\n", friends[i].name, friends[i].age, friends[i].height);
				}
				printf("Complete.\n\n");
				break;
			case '2':
				writeFriend();
				break;
			case '3':
				memset(friends, 0, sizeof(friends));
				num = 0;
				puts("\n\nAll records are deleted.");
				break;
			case '4':
				readFriend();
				break;
			case '9':
				return 0;
		}
	}

	return 0;
}

//	Saving structure array

void writeFriend()
{
	FILE *f;
	struct __header h;

	f = fopen("/Users/sooop/temp/sc/friend.dat", "wb");
	if(f == NULL)
	{
		puts("Can't create file");

	} else {
		strcpy(h.desc, "Friends");
		h.ver = 100;
		h.count = num;

		fwrite(&h, sizeof(struct __header), 1, f);
		fwrite(friends, sizeof(struct __friend), num, f);

		fclose(f);
		puts("saved.");
	}

}

void readFriend()
{
	FILE *f = NULL;
	struct __header h;

	f = fopen("/Users/sooop/temp/sc/friend.dat", "rb");
	if (f == NULL)
	{
		puts("Can't Open file");
	} else {
		memset(friends, 0, sizeof(friends));
		fread(&h, sizeof(struct __header), 1, f);
		if(strcmp(h.desc, "Friends") != 0)
		{
			puts("Not Address File");
			return;
		}
		if (h.ver != 100) {
			puts("Not match version");
			return;
		}
		num = h.count;
		fread(friends, sizeof(struct __friend),num,f);
		puts("read.");
	}

	if(f) fclose(f);

}

여기서 중요한 것은 보통의 프로그램에서 데이터를 담는 구조체는 실제 값을 갖기보다는 포인터를 가지고 있는 경우가 많다. 만약 __friend 구조체가 배열의 포인터만을 갖는다고 하면, 저장하거나 읽어들일 때는 문자열의 크기를 따로 저장해서 읽을 때도 이 정보를 사용해야 한다.

그럼 포인터를 멤버로 갖는 보다 많이 통용되는 모양의 구조체는 어떤 방식으로 디스크에 기록할 수 있을까? 그건 다음 글에서 살펴보기로 하자.