


myhas@LG16Z95P:/mnt/c/Windows/System32/os/ch02$
sudo apt update
sudo apt install gcc
gcc --version


gcc -o osh osh.c
./osh

code .

gcc -o osh osh.c
./osh


Shell 인터페이스는 다음 명령을 입력할 수 있도록 사용자에게 프롬프트를 출력한다. 우리는 osh>이라는 프롬프트가 출력된 후 사용자의 명령을 입력받을 것이다.
Shell 인터페이스를 구현하는 기법 중 하나
다음 코드는 명령줄을 처리하는 Shell 프로그램의 개요를 C언어로 작성한 것이다. main() 함수에서 osh>을 출력하고, 입력된 사용자 명령을 처리하는 개략적 과정이 나와 있다.
스켈레톤 코드의 자세한 해석
#include <stdio.h> // 표준 입출력 함수 (e.g. printf, fgets 등)
#include <unistd.h> // fork(), execvp(), getpid() 등 시스템 호출
#define MAX_LINE 80 // 사용자가 입력할 수 있는 최대 명령어 길이
// 명령어는 최대 80자까지 입력 가능
// -> 명령어 + 공백 + NULL 포함
int main(void)
{
// 사용자가 입력한 명령어를 공백 기준으로 나눈 토큰들 저장할 배열
// (execvp()는 문자열 배열 형태로 인자를 받기 때문에 이렇게 선언)
// 명령어는 보통 2개 단어 정도가 많아서 최대 40개 + NULL 포인터 공간 확보
char *args[MAXLINE / 2 + 1];
// 쉘이 계속 실행될지 결정하는 변수(무한 루프 제어)
// 사용자가 "exit" 명령을 입력하면 0으로 바꿔서 루프 종료
int should_run = 1;
//사용자가 종료 명령을 입력할 때까지 반복
while (should_run) {
printf("osh> "); // 프롬프트 출력 (사용자 입력 대기)
fflush(stdout);
// 출력 버퍼를 강제로 비워서 사용자에게 즉시 프롬프트가 바로 보이도록 함
/**
* 여기에 들어갈 핵심 로직
*
* 1. 사용자로부터 입력을 읽는다 (예: fgets 또는 getline 사용)
* 2. 입력받은 문자열을 공백 단위로 잘라 args[]에 저장 (토큰화)
* 3. fork()를 호출해 자식 프로세스를 생성한다
* 4. 자식 프로세스는 execvp()를 호출하여 명령어 실행
* 5. 부모 프로세스는 자식이 끝날 때까지 wait()로 대기
* 단, 명령어 끝에 `&`가 있으면 백그라운드 실행으로 wait 생략
*/
}
return 0; // 프로그램 종료
}
args[0] = “ps”
args[1] = “-ael”
args[2] = NULL```
execvp(char *command, char *params[])
```command: 실행할 명령#include <stdio.h> // 표준 입출력 함수 (e.g. printf, fgets 등)
#include <unistd.h> // fork(), execvp(), getpid() 등 시스템 호출
#include <string.h> // 문자열 처리 함수 (e.g. strtok, strcmp 등)
#include <sys/types.h> // pid_t 타입 정의
#include <sys/wait.h> // wait() 함수 사용을 위한 헤더
#define MAXLINE 80 // 사용자가 입력할 수 있는 최대 명령어 길이
int main(void)
{
char *args[MAXLINE / 2 + 1];
// 사용자가 입력한 명령어를 공백 기준으로 나눈 토큰들 저장 배열
// 명령어는 보통 2개 단어 정도가 많아서 최대 40개 + NULL 포인터 공간 확보
int should_run = 1;// 프로그램을 계속 실행할지 여부 (무한 루프 제어)
char last_command[MAXLINE];//마지막 명령 저장공간
int has_history = 0;//히스토리 존재 여부
while (should_run) {
printf("osh> ");
fflush(stdout);
// 출력 버퍼를 강제로 비워서 사용자에게 즉시 프롬프트가 바로 보이도록 함
char input[MAXLINE];
//fgets() 띄어쓰기 포함 문자열 사용자 입력을 받고
if (fgets(input, MAXLINE, stdin) == NULL) {
perror("fgets failed");
continue;//사용자 입력 없으면 에러 출력 후 다시 반복
}
//줄 끝의 \n 개행 제거(문자열 끝 처리)
input[strcspn(input, "\n")] = '\0';
// Tokenize input
int arg_index = 0;//배열 인덱스
//공백기준 토큰분리해 포인터저장
char *token = strtok(input, " ");
int background = 0;//명령어 끝에 &여부 확인
//토큰 분석
while (token != NULL && arg_index < MAXLINE / 2) {
if (strcmp(token, "&") == 0) {//&면 백그라운드 on, args에 넣지 않음
background = 1;
} else {
args[arg_index++] = token;//토큰을 args배열에 인덱스별로 삽입해줌
}
token = strtok(NULL, " ");
}
args[arg_index] = NULL;//args는 NULL로 끝나야 execvp()가 작동
//빈 입력이면 continue
if (arg_index == 0) {
continue;
}
//fork()로 명령어를 실행할 자식 프로세스 생성
pid_t pid = fork();
// 백그라운드가 아니면, 부모 프로세스는 자식 wait했다가 종료코드 받으면 실행됨
if (pid > 0) {
if (!background) {
wait(NULL);
}
}
//자식 프로세스 실행
else if (pid == 0) {
// 자식 프로세스
if (execvp(args[0], args) == -1) {
perror("execvp failed");
}
return 1;//실행 실패 시 오류 출력하고 1반환
}
//fork() 실패, 에러 출력 후 다시 루프
else {
perror("Fork failed");
continue;
}
}
return 0;
}

#include <stdio.h> // 표준 입출력 함수 (e.g. printf, fgets 등)
#include <unistd.h> // fork(), execvp(), getpid() 등 시스템 호출
#include <string.h> // 문자열 처리 함수 (e.g. strtok, strcmp 등)
#include <sys/types.h> // pid_t 타입 정의
#include <sys/wait.h> // wait() 함수 사용을 위한 헤더
#include <stdlib.h> // atoi, exit 등 포함
#define MAXLINE 80 // 사용자가 입력할 수 있는 최대 명령어 길이
#define HISTORY_SIZE 10 // 저장할 수 있는 최대 히스토리 개수
int main(void)
{
char *args[MAXLINE / 2 + 1];
// 사용자가 입력한 명령어를 공백 기준으로 나눈 토큰들 저장 배열
// 명령어는 보통 2개 단어 정도가 많아서 최대 40개 + NULL 포인터 공간 확보
int should_run = 1; // 프로그램을 계속 실행할지 여부 (무한 루프 제어)
char history[HISTORY_SIZE][MAXLINE]; // 명령어 히스토리 저장 배열
int history_count = 0; // 현재 저장된 히스토리 수
//사용자가 종료 명령을 입력할 때까지 반복
while (should_run) {
printf("osh> ");
fflush(stdout);
// 출력 버퍼를 강제로 비워서 사용자에게 즉시 프롬프트가 바로 보이도록 함
char input[MAXLINE]; //입력 명령어를 저장할 배열
//fgets() 띄어쓰기 포함 문자열 사용자 입력을 받고
if (fgets(input, MAXLINE, stdin) == NULL) {
perror("fgets failed");
continue;//사용자 입력 없으면 에러 출력 후 다시 반복
}
//줄 끝의 \n 개행 제거(문자열 끝 처리)
input[strcspn(input, "\n")] = '\0';
// !! 처리
if (strcmp(input, "!!") == 0) {
if (history_count == 0) {//히스토리 없으면 메시지 출력
printf("No command in history\n");
continue;
}
//히스토리가 있으면 가장 최근 명령어를 input에 복원해 출력
strcpy(input, history[history_count - 1]);
printf("osh> %s\n", input);
}
// !번호 처리
else if (input[0] == '!' && input[1] != '\0') {
int index = atoi(&input[1]) - 1;
if (index < 0 || index >= history_count) {
printf("No such command in history\n");
continue;
}
// 해당 번호의 명령어를 복원
strcpy(input, history[index]);
printf("osh> %s\n", input);
}
// 히스토리에 명령어 저장
if (history_count < HISTORY_SIZE) {
strcpy(history[history_count++], input);
} else {
// 가장 오래된 명령어를 제거하고 새로운 명령어 추가
for (int i = 1; i < HISTORY_SIZE; i++) {
strcpy(history[i - 1], history[i]);
}
strcpy(history[HISTORY_SIZE - 1], input);
}
// Tokenize input
int arg_index = 0;//배열 인덱스
//공백기준 토큰분리해 포인터저장
char *token = strtok(input, " ");
int background = 0;//명령어 끝에 &여부 확인
//토큰 분석
while (token != NULL && arg_index < MAXLINE / 2) {
if (strcmp(token, "&") == 0) {//&면 백그라운드 on, args에 넣지 않음
background = 1;
} else {
args[arg_index++] = token;//토큰을 args배열에 인덱스별로 삽입해줌
}
token = strtok(NULL, " ");
}
args[arg_index] = NULL;//args는 NULL로 끝나야 execvp()가 작동
//빈 입력이면 continue
if (arg_index == 0) {
continue;
}
//fork()로 명령어를 실행할 자식 프로세스 생성
pid_t pid = fork();
// 백그라운드가 아니면, 부모 프로세스는 자식 wait했다가 종료코드 받으면 실행됨
if (pid > 0) {
if (!background) {
wait(NULL);
}
}
//자식 프로세스 실행
else if (pid == 0) {
// 자식 프로세스
if (execvp(args[0], args) == -1) {
perror("execvp failed");
}
return 1;//실행 실패 시 오류 출력하고 1반환
}
//fork() 실패, 에러 출력 후 다시 루프
else {
perror("Fork failed");
continue;
}
}
return 0;
}


">" 연산자: 명령 실행의 출력을 파일로 저장
"<" 연산자: 명령 실행 입력을 파일로부터 읽어옴
EX) osh> ls > out.txt 입력 시: ls 명령의 출력이 out.txt라는 파일로 저장
osh> sort < in.txt 입력 시: sort의 입력을 in.txt 파일로부터 읽어옴
( "sort < 파일명" : 텍스트 파일에서 입력을 읽어 정렬해줌 )
단 최대 하나의 입력 혹은 출력 재지정만 존재한다고 가정한다. 입출력을 동시에 재지정하지는 않는다고 가정한다.
입출력 재지정을 위해서는 dup2() 함수를 사용한다.
=> dup2() 함수: 이미 존재하는 파일 디스크립터를 다른 파일 디스크립터로 복사한다
EX) fd가 out.txt 파일에 대한 디스크립터일 경우 아래와 같이 fd 디스크립터를 표준 출력에 복사한다.
dup2(fd, STDOUT_FILENO);
즉, 터미널에 출력하는 모든 것이 out.txt 파일에 저장됨을 의미한다.
#include <stdio.h> // 표준 입출력 함수 (e.g. printf, fgets 등)
#include <unistd.h> // fork(), execvp(), getpid() 등 시스템 호출
#include <string.h> // 문자열 처리 함수 (e.g. strtok, strcmp 등)
#include <sys/types.h> // pid_t 타입 정의
#include <sys/wait.h> // wait() 함수 사용을 위한 헤더
#include <fcntl.h> // open(), O_RDONLY 등 사용
#include <stdlib.h> // exit()
#define MAXLINE 80 // 사용자가 입력할 수 있는 최대 명령어 길이
#define HISTORY_SIZE 10 // 저장할 히스토리 최대 개수
int main(void)
{
// 사용자가 입력한 명령어를 공백 기준으로 나눈 토큰들 저장할 배열
// (execvp()는 문자열 배열 형태로 인자를 받기 때문에 이렇게 선언)
char *args[MAXLINE / 2 + 1];
// 쉘이 계속 실행될지 결정하는 변수(무한 루프 제어)
// exit 명령 시 0이 되어 루프 종료
int should_run = 1;
char history[HISTORY_SIZE][MAXLINE]; // 최근 명령어들을 저장하는 히스토리 배열
int history_count = 0; // 저장된 히스토리 개수
// 사용자가 종료 명령을 입력할 때까지 반복
while (should_run) {
printf("osh> ");
fflush(stdout);
//출력 버퍼를 강제로 비워 사용자에게 즉시 프롬프트가 보이도록 함
char input[MAXLINE];//입력 명령어를 저장할 배열
//fgets() 띄어쓰기 포함 문자열 사용자 입력을 받고,
//사용자 입력 없으면 에러 출력 후 다시 반복
if (fgets(input, MAXLINE, stdin) == NULL) {
perror("fgets failed");
continue;
}
//입력 문자열 끝의 개행 문자 제거(문자열 끝 처리)
input[strcspn(input, "\n")] = '\0';
//1. !번호 처리 (예: !3 은 3번째 명령 실행)
if (input[0] == '!' && input[1] != '!') {
int index = atoi(&input[1]) - 1; // 입력된 숫자 -1 (0부터 시작)
if (index < 0 || index >= history_count) {
printf("No such command in history\n");
continue;
}
strcpy(input, history[index]);
printf("osh> %s\n", input);
}
//2. !! 처리 (가장 최근 명령 실행)
else if (strcmp(input, "!!") == 0) {
//히스토리가 없으면 메시지 출력
if (history_count == 0) {
printf("No command in history\n");
continue;
}
//히스토리 있으면 이전 명령어를 input에 복사해 출력
strcpy(input, history[history_count - 1]);
printf("osh> %s\n", input);
}
// 입력된 명령어를 히스토리에 저장한다
if (history_count < HISTORY_SIZE) {
strcpy(history[history_count++], input);
} else {
//갱신 방식: 가장 오래된 명령어 제거 후 새 명령 추가 (FIFO 방식)
//명령어를 한칸씩 당겨 저장해 history[0] 명령어 제거
for (int i = 1; i < HISTORY_SIZE; i++) {
strcpy(history[i - 1], history[i]);
}
strcpy(history[HISTORY_SIZE - 1], input);
}
// Tokenize input
int arg_index = 0;//args배열 인덱스
//공백기준 토큰 분리해 포인터 저장
char *token = strtok(input, " ");
int background = 0;//명령어 끝에 &여부 확인 플래그
char *infile = NULL;//< 입력 재지정
char *outfile = NULL;//> 출력 재지정
//입력된 명령어를 공백 기준 토큰단위로 분석
while (token != NULL && arg_index < MAXLINE / 2) {
// strcmp결과 두문자가 정확히 같으면 0반환해 조건문 실행
//경우1. 명령어 끝에 & 붙어 백그라운드 실행
if (strcmp(token, "&") == 0) {
background = 1;
}
//경우2. < 연산자 다음의 파일명을 infile에 저장한다.
else if (strcmp(token, "<") == 0) {
token = strtok(NULL, " ");
if (token != NULL) infile = token; // 입력 리디렉션 파일 지정
}
//경우3. > 연산자 다음의 파일명을 outfile에 저장한다.
else if (strcmp(token, ">") == 0) {
token = strtok(NULL, " ");
if (token != NULL) outfile = token; // 출력 리디렉션 파일 지정
}
//경우4. 앞의 문자들에 해당 안되는 일반 명령이면 그냥 args 배열에 저장
else {
args[arg_index++] = token;
}
//token 변수에 다음 토큰으로 갱신해 저장
token = strtok(NULL, " ");
}
//args는 NULL로 끝나야 execvp()가 작동되므로 마지막에 대입
args[arg_index] = NULL;
//만약 빈 입력이면 osh>에 다시 입력받음
if (arg_index == 0) continue;
//fork()로 명령어를 실행할 자식 프로세스 생성
pid_t pid = fork();
//1. 부모 프로세스 실행
if (pid > 0) {
// 백그라운드가 아니면 자식 종료까지 대기
if (!background) {
wait(NULL);
}
}
//2. 자식 프로세스 실행
else if (pid == 0) {
//2.1 infile에 내용이 있으면 < 입력 리디렉션 처리
if (infile != NULL) {
//file을 R모드로 열어 파일 디스트립터(fd)를 반환
int fd = open(infile, O_RDONLY);
if (fd < 0) {//파일 열기 실패시 에러메시지 출력 후 자식 종료
perror("open (input) failed");
exit(1);
}
//표준 입력(0)을 fd로 복제한다.
//즉 프로그램이 입력을 받으면 파일에서 읽도록 함
dup2(fd, STDIN_FILENO);
close(fd);
}
//2.2 > 출력 리디렉션 처리
if (outfile != NULL) {
//outfile을 쓰기모드로 열되, 파일이 없으면 생성(create), 있으면 내용을 비운다(trunc).
int fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {//파일 열기 실패 시 에러메세지 출력 후 자식 종료
perror("open (output) failed");
exit(1);
}
//프로그램의 표준 출력을 이제 파일에서 출력
dup2(fd, STDOUT_FILENO);
close(fd);//dup2로 복사했으므로 fd는 close
}
//3. 명령어 실행 execvp로 현재 프로세스를 새 프로그램으로 교체
if (execvp(args[0], args) == -1) {
perror("execvp failed");
}
return 1; //종료코드 반환해 자식 프로세스 종료
}
//fork() 실패 시, 에러 출력 후 다시 루프
else {
perror("Fork failed");
continue;
}
}
return 0;
}
