[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++의 예제의 내용을 사용하고 있습니다.]

지난 글에서는 구조체로 정의한 임의의 정보들을 디스크에 기록하는 방법을 예시를 통해 살펴보았다. 그런데 그 글에서의 예제는 실제 기록하고 읽어들일 때 사용하는 함수의 사용법에 초점을 맞추었던 관계로 모든 정보가 구조체 내부에 들어있는 상황을 가정했다.

하지만 실제 세상에서는 구조체 내부에 배열을 직접 쓰는 일보다는 포인터를 쓰는 일이 더 흔하다. 이 말은 어떤 단위를 구성하는 구조체를 정의했을 때, 그 단위 객체의 속성을 이루는 정보들은 구조체가 점유하는 메모리 영역의 외부에 있을 수 있다는 말이다.

struct Friend1 {
  char name[64];
  int age;
  double height;
};

struct Friend2 {
  char* name;
  int age;
  double height;
};

위 코드에서 두 구조체를 보면 struct Friend1의 경우에는 내부에 char 배열을 가지고 있다. 따라서 실제 문자열 데이터는 구조체가 점유하고 있는 메모리 공간내에 존재한다. 하지만 두 번째 struct Friend2에는 내부에 char* 타입의 포인터를 가지고 있다. 이 말은 이 구조체의 name 멤버는 구조체 외부에 저장되어 있는 문자열 데이터를 가리키게 된다는 말이다.

따라서 fwrite를 사용해서 파일에 이를 기록하는 경우, 실제 문자열 값은 저장되지 않는다. name 필드는 메모리 주소값을 가리키는 부호 없는 정수와 같기 때문에 숫자값만이 저장될 것이고, 다음 번 프로그램 실행 시에는 이 숫자값이 가리키는 곳에는 문자열이 아닌 엉뚱한 값이 저장되기 때문에 프로그램이 제대로 동작하지 않을 것이다.

파일에 정보를 기록할 때, xml이나 json과 같은 특정한 포맷을 따르지 않고 직접 정의한 포맷을 사용하려 한다면 두 가지 원칙을 잘 기억하고 따라야 한다. 첫번째는 필요한 모든 정보를 모두 기록해야 한다는 것과, 두 번째는 기록된 정보들은 명시적이든 암묵적이든 온전하게 읽어서 복구할 수 있어야 한다는 것이다.

Friend1, Friend2 의 내부의 정보 구성은 대략 다음과 같다. (각 타입의 align에 따라서 Friend1의 전체 크기 중에는 비어서 쓰지 않는 공간이 있을 수 있겠지만 이는 무시한다.) Friend2.name 은 포인터이며 이는 외부의 문자열을 참조하며, 그 길이는 현재 알지 못한다.

# Friend1
[name(char[64]) .....  ][age(int)][height(double)]
 ~~~~~~~~~~~~~~~~~~~~~~

# Friend2
[name(char*)][age(int)][height(double)]
   ↳ [char][char][char][char].....

따라서 Friend2를 저장할 때에는 구조체 정보 자체를 저장하는 것이 아니라 모든 멤버를 쪼게어 다음과 같이 저장한다.

  1. 문자열은 그 크기를 알아야 하므로 멤버 name이 가리키는 문자열의 길이를 구한다.
  2. 구해진 문자열의 길이를 저장한다.
  3. 그 길이만큼의 문자열을 저장한다.
  4. age 등 크기가 고정된 다른 멤버들을 저장한다.

만약 크기가 고정되지 않은 문자열이나 배열등의 포인터를 저장해야 한다면 모두 길이를 먼저 기록하고 각각의 데이터를 기록해야 한다.

#define MYFILENAME "friends2.dat"

typedef struct _friend {
  char * name;
  int age;
  double height;
} Friend;

int num = 4;
Friend friends[256] = { .... };

데이터가 이렇게 생겼을 때 이를 파일에 저장하는 함수를 만들어보자.

// Friend[] 타입의 데이터를 파일에 기록
// 데이터 개수는 num 에 있음
// 각 데이터를 저장하는 순서는 다음과 같음
// [name길이][name .....(가변크기).......][age][height]
//
void writeFriend() {
  int i, l;
  FILE * f;
  f = fopen(MYFILENAME, "wb");
  if(f == NULL) {
    printf("\nCan't open file.\n");
    return;
  }

  // 데이터 개수 기록
  fwrite(&num, sizeof(int), 1, f);
  for(i=0; i<num; i++) {
    // 각 데이터 기록
    // 이름 길이 기록
    l = strlen(friends[i].name);
    fwrite(&l, sizeof(int), 1, f);
    // 이름 기록
    fwrite(friends[i].name, sizeof(char), l, f);
    // 그외 숫자값 멤버 기록
    fwrite(&friends[i].age, sizeof(int), 1, f);
    fwrite(&friends[i].height, sizeof(double), 1, f);
  }
  fclose(f);
}

기록할 문자열의 길이를 strlen 으로 구하기만 하면 기록하는 과정은 낱개의 여러 데이터를 순차적으로 기록하는 것이며 그리 어려운 점은 없어 보인다. 다음은 저장된 데이터를 읽어와서 friends 배열을 복원하는 함수를 작성할 차례이다. 그런데 이 과정에서 현재 메모리의 데이터를 초기화하는 부분이 들어간다. 이 부분을 따로 deleteFriends() 함수로 분리하고 작성할 것이다.

void deleteFriends()
{
  // 데이터 초기화
  // 각 데이터의 이름 영역을 해제하고 데이터 배열을 비운다.
  // name으로 사용되는 영역은 모두 동적으로 할당해주었으므로
  // 직접 해제하여야 한다.
  int i;
  for(i=0;i<num;i++) {
    free(friends[i].name);
  }
  memset(friends, 0, sizeof(friends));
  num = 0;
}

실제 데이터를 읽어서 구조체를 복원하는 동작은 다음과 같은 순서로 실행한다.

  1. Friend 타입 구조체 변수를 하나 선언한다.
  2. 첫번째로 int 값 하나을 읽는다. 이는 name의 길이이다.
  3. 1의 길이크기 + 1 만큼의 char 타입 메모리를 할당받는다.
  4. 2에 1의 크기만큼 데이터를 읽는다. (문자열) 그리고 마지막 바이트는 0으로 넣어준다.
  5. 4의 문자열 주소를 Friend.name 으로 할당한다
  6. int 값 하나를 읽는다 (Friend.age)
  7. double 값 하나를 읽는다. (Friend.height)
  8. 구조체의 정보들이 초기화가 마쳐졌으므로 friends 배열에 넣어준다.
void readFriends() {
  int i, l;
  FILE * f;
  f = fopen(MYFILENAME, "rb");
  if(f==NULL) {
    printf("\nCan't open file.\n");
    return;
  }

  // 파일을 열었으니 배열을 초기화한다.
  deleteFriends();
  
  // 배열 크기를 읽고 그만큼 반복
  fread(&num, sizeof(int), 1, f);
  for(i=0;i<num;i++){
    // 변수 선언
    Friend p;
    char * name;
    // 이름 길이를 읽고, 필요한 만큼 메모리 할당
    // 그 크기만큼 파일에서 이름을 읽는다.
    fread(&l, sizeof(int), 1, f);
    name = malloc(sizeof(char)*(l+1));
    fread(name, sizeof(char), l, f);
    name[l] = 0;
    p.name = name;
    // age, height 읽기
    fread(&p.age, sizeof(int), 1, f);
    fread(&p.height, sizeof(double), 1, f);
    
    // 배열에 추가
    friends[i] = p;
  }
}

전체적인 코드는 아래와 같다. 배열의 초기화 코드를 작성하기 귀찮아서 수동으로 입력받아서 생성할 수 있게 하였다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MYFILENAME "friends2.dat"

void writeFriends();
void readFriends();
void addFriends();
void deleteFriends();

typedef struct _friend {
  char * name;
  int age;
  double height;
} Friend;

typedef struct _header {
  char desc[100];
  int version;
  int count;
} header;

int num = 0;
Friend friends[20] = {};

int main(void) {
  int i;
  char sel;

  for(;;) {
    printf("\n\n\n1.show  2.save  3.delete  4.read 5.add 9.quit:  ");
    sel = getchar();
    switch(sel) {
      case '1':
        for(i=0;i<num;i++) {
          printf("no. %i\nName: %s\nAge: %d\nHeight: %.1f\n\n",
                  i+1,
                  friends[i].name,
                  friends[i].age,
                  friends[i].height
              );
        }
        break;
      case '2':
        writeFriends();
        break;
      case '3':
        deleteFriends();
        break;
      case '4':
        readFriends();
        break;
      case '5':
          addFriends();
          break;
      case '9':
          return 0;
      default:
          printf("\nEnter again.\n");
    }
    fflush(stdin);
  }
  return 0;
}


void writeFriends() {
  FILE *f;
  f = fopen(MYFILENAME, "wb");
  if(f==NULL) {
    printf("Fail to open file\n");
    return;
  }
  // 헤더기록
  header h;
  char* msg = "friends";
  strcpy(h.desc, msg);
  h.version = 101;
  h.count = num;
  fwrite(&h, sizeof(header), 1, f);

  int i, l;
  // 각각의 데이터 기록
  for(i=0;i<num;i++) {
    // [(int)글자수][문자열][int][double]
    l = strlen(friends[i].name);
    fwrite(&l, sizeof(int), 1, f);
    fwrite(friends[i].name, sizeof(char), l, f);
    fwrite(&(friends[i].age), sizeof(int), 1, f);
    fwrite(&(friends[i].height), sizeof(double), 1, f);
  }
  if(f) {
    fclose(f);
  }
}

void addFriends() {
  char buffer[1024];
  int age;
  double height;
  int l;
  Friend f;
  printf("Name age height: ");
  scanf("%s %d %lf", buffer, &age, &height);

  l = strlen(buffer);
  char *name = malloc(sizeof(char) * (l + 1));
  memcpy(name, buffer, l);
  name[l] = 0;

  f.name = name;
  f.age = age;
  f.height = height;

  friends[num] = f
  num += 1;
}


void readFriends() {
  int i, l;
  header h;
  FILE * f;

  f = fopen(MYFILENAME, "rb");
  if(f==NULL) {
    printf("\nCan't open file.\n");
    return;
  }
  
  // 헤더 검사
  fread(&h, sizeof(header), 1, f);
  if(strcmp(h.desc, "friends") != 0) {
    printf("\nInvalid File Format\n");
    exit(1);
  }
  if(h.version != 101) {
    printf("\nNot valid data version\n");
    return;
  }

  // 데이터읽기
  deleteFriends(); // 읽기 전 초기화
  num = h.count;
  for(i=0;i<num;i++){
    Friend p;
    fread(&l, sizeof(int), 1, f);
    char *name = malloc(sizeof(char)*(l+1));
    fread(name, sizeof(char), l, f);
    name[l] = 0;
    p.name = name;
    fread(&p.age, sizeof(int), 1, f);
    fread(&p.height, sizeof(double), 1, f);
    friends[i] = p;
  }
}

void deleteFriends()
{
  // 데이터 초기화
  // 각 데이터의 이름 영역을 해제하고
  // 데이터 배열을 비운다.
  int i;
  for(i=0;i<num;i++) {
    free(friends[i].name);
  }
  memset(friends, 0, sizeof(friends));
  num = 0;

}

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

이번 글은 구조체를 파일로 저장하고 읽어들이는 작업을 예제로 살펴보는 것이다. 원리는 간단하다. 첫번째로 파일 스트림을 사용해서 데이터를 읽고 쓴다. 여기에는 fwrite(), fread()라는 두 함수를 사용하는데 두 함수로 전달되는 인자는 파일 디스크립터와 읽고 쓰는 데이터의 크기로 사용법은 거의 비슷하다. 두 번째 원리는 각 멤버를 순서대로 기록하는 것이다. 이 때 순서는 중요한데, 왜냐하면 읽어들일 때에도 기록한 순서 그대로 읽어들이면 되는 것이다.

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