# 유닉스 유틸리티 프로젝트

```{note}
**시작하기 전에:** [실습 튜토리얼](https://os2024.halla.ai/projects/lab-tutorial.html)을 읽어보세요. C 프로그래밍 환경에서 코딩할 때 유용한 팁들이 있습니다.
```

이 프로젝트에서는 `cat`, **ls** 등 자주 사용되는 유닉스 명령어들의 간단한 버전을 직접 구현해 볼 것입니다. 혼동을 피하기 위해 각 명령어의 이름을 약간 다르게 지어 보겠습니다. 예를 들어, `cat` 대신 `hcat`(즉, "halla" cat)을 구현할 것입니다.

## 목표

- C 프로그래밍 언어에 다시 익숙해지기
- 유닉스 셸, 터미널, 명령줄에 다시 익숙해지기
- (부수적 효과로) 이맥스와 같은 적절한 코드 편집기 사용법 배우기
- 유닉스 유틸리티가 어떻게 구현되는지 살짝 배우기

이 프로젝트는 간단한 C 프로그램을 작성하는 데 초점을 맞추고 있지만, 셸이 무엇인지, 유닉스 기반 시스템(예: 리눅스 또는 macOS)의 명령줄을 어떻게 사용하는지, 이맥스와 같은 편집기를 어떻게 사용하는지, 그리고 물론 C 프로그래밍의 기본적인 이해 등 많은 사전 지식이 필요합니다. 이러한 기술들이 아직 없다면 여기서 시작하기에는 적절하지 않을 수 있습니다.

제출해야 할 것들을 요약하면:

- 아래의 각 유틸리티에 대해 하나의 .c 파일: **hcat.c**, **hgrep.c**, **hzip.c**, **hunzip.c**.
- 각 파일은 **-Wall**과 **-Werror** 플래그로 컴파일할 때 오류 없이 컴파일되어야 합니다.
- 각 프로그램은 우리가 제공하는 테스트를 (바라건대) 통과해야 합니다.

## hcat

`hcat` 프로그램은 간단합니다. 일반적으로 사용자가 지정한 파일을 읽어 그 내용을 출력합니다. 전형적인 사용법은 다음과 같습니다. 사용자가 main.c의 내용을 보고 싶어서 아래와 같이 입력합니다:

```bash
./hcat main.c
```

```c
#include <stdio.h>
...
```

위에서 보여준 것처럼, `hcat`은 **main.c** 파일을 읽고 그 내용을 출력합니다. 위의 `hcat` 앞에 있는 "**./**"는 유닉스의 관례입니다. 시스템에 `hcat`이 어떤 디렉토리에 있는지 알려주는 것입니다(이 경우는 현재 작업 디렉토리를 의미하는 "."(점) 디렉토리입니다).

`hcat` 바이너리를 만들기 위해 **hcat.c**라는 하나의 소스 파일을 생성하고 `cat`의 단순화된 버전을 구현하는 약간의 C 코드를 작성할 것입니다. 이 프로그램을 컴파일하려면 다음과 같이 하면 됩니다:

```bash
gcc -o hcat hcat.c -Wall -Werror
```

이렇게 하면 `hcat`이라는 실행 가능한 바이너리가 만들어지고, 위에서처럼 실행할 수 있습니다.

이 프로그램의 소스 코드(우리는 **hcat.c**에 있다고 가정합니다)를 구현하기 위해서는 C 표준 라이브러리(흔히 **libc**라고 불립니다)의 몇 가지 라이브러리 루틴을 사용하는 방법을 배워야 합니다. 모든 C 코드는 자동으로 유용한 함수들로 가득 찬 C 라이브러리와 링크됩니다. C 라이브러리에 대해 [여기](https://en.wikipedia.org/wiki/C_standard_library)와 [여기](https://www-s.acm.illinois.edu/webmonkeys/book/c_guide/)에서 더 알아보세요.

이 프로젝트에서는 파일 입출력을 위해 다음과 같은 루틴을 사용하는 것이 좋습니다: `fopen()`, `fgets()`, **fclose()**. 이렇게 새로운 함수를 사용할 때마다 가장 먼저 해야 할 일은 그것에 대해 읽어보는 것입니다. 그렇지 않으면 어떻게 제대로 사용하는지 배울 수 있겠습니까?

유닉스 시스템에서 이런 함수들에 대해 알아보는 가장 좋은 방법은 **man** 페이지(**manual**의 줄임말)를 사용하는 것입니다. HTML/웹 중심의 세상에서 맨 페이지는 약간 구식으로 느껴질 수 있지만, 유용하고 유익하며 일반적으로 사용하기 매우 쉽습니다.

예를 들어, `fopen()`의 man 페이지에 접근하려면 유닉스 셸 프롬프트에서 다음과 같이 입력하기만 하면 됩니다:

```bash
man fopen
```

그리고 읽으세요! man 페이지를 효과적으로 읽는 것은 연습이 필요합니다. 지금 바로 배우는 게 어떨까요?

여기서 간단한 개요를 드리겠습니다. `fopen()` 함수는 파일을 "열어줍니다". 유닉스 시스템에서 파일 접근을 시작하는 일반적인 방법입니다. 이 경우 파일을 여는 것은 그냥 **FILE** 타입의 구조체에 대한 포인터를 반환하는 것이고, 이 포인터는 읽기, 쓰기 등을 하기 위해 다른 루틴에 전달될 수 있습니다.

아래는 `fopen()`의 전형적인 사용법입니다:

```c
FILE *fp = fopen("main.c", "r");
if (fp == NULL) {
    printf("cannot open file\n");
    exit(1);
}
```

몇 가지 주의할 점이 있습니다. 첫째, `fopen()`은 두 개의 인자를 받습니다: 파일의 *이름*과 *모드*입니다. 모드는 그 파일로 무엇을 할 계획인지를 나타냅니다. 이 경우 파일을 읽고 싶기 때문에 두 번째 인자로 "r"을 전달합니다. 다른 옵션이 무엇이 있는지 맨 페이지를 읽어보세요.

둘째, `fopen()`이 실제로 성공했는지에 대한 _중요한_ 확인 작업에 주목하세요. 이것은 무언가 잘못되었을 때 예외가 발생하는 자바가 아닙니다. C이기 때문에 좋은 프로그램(즉, 여러분이 작성하고 싶어하는 유일한 종류의 프로그램)에서는 호출이 성공했는지 항상 확인해야 합니다. 맨 페이지를 읽으면 오류가 발생했을 때 무엇이 반환되는지에 대한 세부 사항을 알 수 있습니다. 이 경우 macOS 맨 페이지에는 다음과 같이 나와 있습니다:

```
Upon successful completion fopen(), fdopen(), freopen() and fmemopen() return
a FILE pointer.  Otherwise, NULL is returned and the global variable errno is
set to indicate the error.
```

따라서 위의 코드에서 했던 것처럼 FILE 포인터를 사용하기 전에 `fopen()`이 NULL을 반환하지 않는지 확인하세요.

셋째, 오류가 발생했을 때 프로그램이 메시지를 출력하고 오류 상태 코드 1로 종료한다는 점에 주목하세요. 유닉스 시스템에서는 성공 시 0을 반환하고 실패 시 0이 아닌 값을 반환하는 것이 전통입니다. 여기서는 실패를 나타내기 위해 1을 사용할 것입니다.

참고: `fopen()`이 실패할 경우 그 이유는 많이 있을 수 있습니다. 오류가 왜 발생했는지 더 자세히 출력하기 위해서는 `perror()` 또는 `strerror()` 함수를 사용할 수 있습니다. 스스로 알아보세요(여러분이 맞췄습니다... 맨 페이지를 사용하세요!).

파일을 열고 나면 그것으로부터 읽는 방법이 많이 있습니다. 여기서 제안하는 방법은 `fgets()`를 사용하는 것인데, 이것은 파일에서 한 번에 한 줄씩 입력을 받는 데 사용됩니다.

파일 내용을 출력하려면 그냥 `printf()`를 사용하면 됩니다. 예를 들어, `fgets()`로 한 줄을 **buffer**라는 변수에 읽어들인 후에는 그냥 다음과 같이 buffer를 출력하면 됩니다:

```c
printf("%s", buffer);
```

`printf()`에 개행 문자(`\n`)를 추가하면 안 된다는 점에 주의하세요. 그러면 파일의 출력에 추가적인 개행이 생기게 됩니다. 읽어들인 buffer의 정확한 내용을 출력하세요(물론 개행을 포함할 수도 있습니다).

마지막으로, 읽기와 출력이 끝나면 `fclose()`를 사용하여 파일을 닫으세요(더 이상 그 파일로부터 읽지 않는다는 것을 나타냅니다).

**세부사항**

- `hcat` 프로그램은 명령줄에서 하나 이상의 파일과 함께 실행될 수 있습니다. 각 파일을 차례로 출력하면 됩니다.
- 오류가 아닌 모든 경우에 `hcat`은 상태 코드 0으로 종료되어야 합니다. 보통 `main()`에서 0을 반환하거나 `exit(0)`을 호출하면 됩니다.
- 명령줄에 _파일이 지정되지 않은_ 경우, `hcat`은 그냥 종료하고 0을 반환해야 합니다. 이것이 일반적인 유닉스 `cat`의 동작과는 약간 다르다는 점에 유의하세요(원한다면 차이점을 알아내 보세요).
- 프로그램이 `fopen()`으로 파일을 열려고 시도했는데 실패한 경우, "hcat: cannot open file"이라는 정확한 메시지를 출력하고(개행 문자가 뒤따라야 함) 상태 코드 1로 종료해야 합니다. 명령줄에 여러 파일이 지정된 경우, 파일 목록의 끝에 도달하거나 파일을 여는 데 오류가 발생할 때까지(이 경우 오류 메시지를 출력하고 `hcat`이 종료됩니다) 파일들을 순서대로 출력해야 합니다.

## hgrep

여러분이 만들 두 번째 유틸리티는 `hgrep`이라고 하는데, 유닉스 도구 `grep`의 변형입니다. 이 도구는 파일을 한 줄씩 살펴보면서 그 줄에 사용자가 지정한 검색어가 있는지 확인합니다. 한 줄에 그 단어가 있으면 그 줄이 출력되고, 그렇지 않으면 출력되지 않습니다.

사용자가 **bar.txt** 파일에서 **foo**라는 용어를 찾는 방법은 다음과 같습니다:

```bash
./hgrep foo bar.txt
```

```
this line has foo in it
so does this foolish line; do you see where?
even this line, which has barfood in it, will be printed.
```

**세부사항**

- `hgrep` 프로그램에는 항상 검색어와 0개 이상의 검색할 파일이 명령줄을 통해 전달됩니다(따라서 하나 이상이 가능합니다). 각 줄을 살펴보면서 검색어가 있는지 확인해야 합니다. 있으면 그 줄을 출력하고, 없으면 건너뛰어야 합니다.
- 일치하는 것은 대소문자를 구분합니다. 따라서 **foo**를 검색할 경우 **Foo**가 있는 줄은 일치하지 _않습니다_.
- 줄은 임의의 길이일 수 있습니다(즉, 개행 문자 `\n`을 만나기 전에 매우 많은 문자가 나올 수 있습니다). `hgrep`은 아주 긴 줄에서도 예상대로 작동해야 합니다. 이를 위해 `fgets()` 대신 `getline()` 라이브러리 호출을 살펴보거나 직접 구현해볼 수 있습니다.
- `hgrep`에 명령줄 인자가 전달되지 않으면 "hgrep: searchterm [file ...]"을 출력하고(개행이 뒤따라야 함) 상태 코드 1로 종료해야 합니다.
- `hgrep`이 열 수 없는 파일을 만나면 "hgrep: cannot open file"을 출력하고(개행이 뒤따라야 함) 상태 코드 1로 종료해야 합니다.
- 다른 모든 경우에 `hgrep`은 리턴 코드 0으로 종료되어야 합니다.
- 검색어는 지정됐지만 파일이 지정되지 않은 경우, `hgrep`은 파일에서 읽는 대신 *표준 입력*에서 읽어야 합니다. 파일 스트림 **stdin**이 이미 열려 있기 때문에 이렇게 하는 것은 쉽습니다. `fgets()` (또는 유사한 루틴)을 사용하여 거기서 읽을 수 있습니다.
- 단순화를 위해 검색 문자열로 빈 문자열이 전달되면 `hgrep`은 어떤 줄도 일치하지 않거나 모든 줄이 일치하는 것으로 처리할 수 있습니다. 둘 다 허용됩니다.

## hzip과 hunzip

다음으로 만들 도구는 한 쌍으로 제공됩니다. 하나(`hzip`)는 파일 압축 도구이고 다른 하나(`hunzip`)는 파일 압축 해제 도구이기 때문입니다.

여기서 사용되는 압축 유형은 _런 길이 인코딩_(Run-Length Encoding, RLE)이라고 하는 간단한 형태의 압축입니다. RLE은 매우 간단합니다: 연속해서 나타나는 **n**개의 같은 문자를 만나면 압축 도구(`hzip`)는 그것을 숫자 **n**과 해당 문자의 한 인스턴스로 바꿉니다.

따라서 다음과 같은 내용의 파일이 있다면:

```
aaaaaaaaaabbbb
```

이 도구는 (논리적으로) 그것을 다음과 같이 바꿀 것입니다:

```
10a4b
```

하지만 압축된 파일의 정확한 형식이 매우 중요합니다. 여기서는 바이너리 형식의 4바이트 정수를 쓴 다음 ASCII로 된 단일 문자를 씁니다. 따라서 압축된 파일은 각각 4바이트 정수(런 길이)와 단일 문자로 구성된 일정 수의 5바이트 항목으로 구성됩니다.

바이너리 형식(ASCII가 아님)으로 정수를 쓰려면 `fwrite()`를 사용해야 합니다. 자세한 내용은 맨 페이지를 읽어보세요. `hzip`의 경우 모든 출력은 표준 출력(프로그램이 실행을 시작할 때 이미 열려 있는 **stdout** 파일 스트림)에 써야 합니다.

`hzip` 도구의 일반적인 사용법은 셸 리디렉션을 사용하여 압축된 출력을 파일에 쓰는 것임을 유의하세요. 예를 들어 **file.txt** 파일을 (바라건대 더 작은) **file.z**로 압축하려면 다음과 같이 입력합니다:

```bash
./hzip file.txt > file.z
```

"큰 따옴표" 기호는 유닉스 셸 리디렉션입니다. 이 경우 `hzip`의 출력이 (화면에 출력되는 대신) **file.z**라는 파일에 쓰여지도록 합니다. 이것이 어떻게 작동하는지에 대해서는 이 강좌에서 좀 더 배우게 될 것입니다.

`hunzip` 도구는 간단히 `hzip` 도구의 반대 작업을 수행합니다. 압축된 파일을 받아서 (다시 표준 출력으로) 압축 해제된 결과를 씁니다. 예를 들어 **file.txt**의 내용을 보려면 다음과 같이 입력합니다:

```bash
./hunzip file.z
```

`hunzip`은 압축된 파일을 읽고(`fread()`를 사용할 것입니다) `printf()`를 사용하여 압축 해제된 출력을 표준 출력에 출력해야 합니다.

**세부사항**

- 올바른 호출은 명령줄을 통해 하나 이상의 파일을 프로그램에 전달해야 합니다. 파일이 지정되지 않은 경우 프로그램은 리턴 코드 1로 종료되어야 하며 `hzip`과 `hunzip`에 대해 각각 "hzip: file1 [file2 ...]"(개행 문자가 뒤따라야 함) 또는 "hunzip: file1 [file2 ...]"(개행 문자가 뒤따라야 함)을 출력해야 합니다.
- 압축된 파일의 형식은 위의 설명과 정확히 일치해야 합니다(각 실행에 대해 4바이트 정수 뒤에 문자가 옵니다).
- 여러 파일이 `hzip`에 전달되면 단일 압축된 출력으로 압축되고, 압축 해제 시 단일 압축 해제된 텍스트 스트림이 된다는 점에 유의하세요(따라서 원래 `hzip`에 여러 파일이 입력되었다는 정보는 손실됩니다). `hunzip`에도 같은 내용이 적용됩니다.

이 프로젝트 테스터를 활용하면 체계적이고 효과적으로 코드를 테스트하고 개선해 나갈 수 있습니다. 명세에 집중하면서 TDD(테스트 주도 개발) 방식으로 개발을 진행해 보는 것도 좋은 경험이 될 것입니다.

코드 작성 시에는 가독성과 효율성을 함께 고려해야 합니다. 적절한 변수명과 함수명을 사용하고, 주석을 충실히 달며, 코드를 논리적인 블록으로 구성하세요. 불필요한 복잡성을 피하고 간결하고 이해하기 쉬운 코드를 작성하도록 노력하세요.

디버깅은 프로그래밍의 필수불가결한 부분입니다. 테스트가 실패할 경우 gdb와 같은 디버거를 사용하여 문제의 원인을 찾아내세요. 코드를 단계별로 실행하고 변수의 값을 확인하며 로직의 흐름을 추적하세요. 디버깅 기술을 연마하면 문제 해결 능력이 크게 향상될 것입니다.

마지막으로 동료들과 협업하고 코드 리뷰를 활용하세요. 다른 사람의 시각에서 코드를 바라보면 개선점과 버그를 발견하기 쉽습니다. 서로의 코드를 리뷰하고 건설적인 피드백을 제공하세요. 협업을 통해 더 나은 코드와 문제 해결 방법을 배울 수 있습니다.

좋은 코드와 꾸준한 연습을 통해 여러분의 프로그래밍 실력은 한층 더 성장할 것입니다. 행운을 빕니다!
