Skip to main content

Command Palette

Search for a command to run...

Understanding the Shell

Published
27 min read
Understanding the Shell
H
Hi there, I'm a full time software engineering student, and full time mum based in Brisbane, QLD, Australia. Korean is my native language and English is my second, but I love learning software in English. My posts are a mix of both Korean and English, so there's something for everyone! All my posts come straight from my lecture notes, so follow along my study journey with me <3

1️⃣Understanding the Shell
2️⃣Shell Scripts

Shell의 이해

Shell(쉘)은 하나의 컴퓨터 프로그램(computer program)이다. C, Python, Java처럼 익숙한 프로그래밍 언어들과는 다소 생소하게 느껴질 수 있지만, 쉘 자체도 프로그래밍 언어(programming language)의 일종이며 시스템 전반에서 광범위하게 사용된다.

쉘은 세 가지 측면에서 이해할 수 있다. 첫째로 macro processor로서, 문자(character)와 기호(symbol)를 조합해 다양한 표현이 가능하다. 둘째로 command interpreter(명령어 해석기)로서, 사용자가 입력한 명령을 해석하고 실행한다. 셋째로 앞서 언급했듯 programming language이기도 하다. C, Java, Python을 다뤄봤다면 이 언어들 사이에 공통된 구조적 특성이 있다는 것을 느꼈을 텐데, 쉘도 그러한 언어적 특성을 공유한다.

실용적인 측면에서 쉘은 개발자가 배포(deployment)나 시스템 관리(system administration)를 수행할 때 매우 유용하게 쓰인다. 동작 방식은 두 가지로, 키보드 입력을 통해 대화형(interactive)으로 실행하거나, 파일이나 문자열(string)을 읽어 실행할 수 있다. 이때 파일로 저장된 형태가 바로 shell script(쉘 스크립트)이며, 문자열을 읽는다는 것은 컴파일된 binary(바이너리) 코드가 아닌 사람이 읽을 수 있는 형태, 즉 human-readable한 형태를 의미한다.

마지막으로 중요한 개념이 하나 있다. 쉘에서는 실행 중인 명령이 완료될 때까지 기다린 후 다음 명령을 실행하는 것이 일반적이다. 그러나 명령이 완료되지 않은 상태에서도 추가 명령을 실행할 수 있다. 프로그래밍은 본질적으로 명령의 순차적 나열(sequential execution)인데, 이 개념은 그 순서를 벗어나는 것이다. 이것이 무엇을 의미하는지 생각하며 계속 쉘에 대해 알아보자


Shell의 역할

Shell(쉘)은 사용자(user)와 커널(kernel) 사이의 상호작용(interaction)을 중개하는 프로그램이다. 여기서 사용자란 컴퓨터 앞에서 입력과 출력을 주고받는 주체로, end user일 수도 있고 개발자(developer)일 수도 있다. 커널이 운영체제의 핵심 알맹이라면, 쉘은 그것을 감싸는 껍질이다. 즉, 사용자는 껍질인 쉘을 통해 알맹이인 커널에 접근한다고 이해하면 쉽다.

쉘은 이러한 중개 역할 외에도 사용자 편의 기능을 제공한다. 대표적인 예가 tab completion(탭 자동완성)이다. 예를 들어 ABCDEF라는 명령어가 있을 때, AB만 입력하고 Tab 키를 누르면 나머지가 자동으로 완성된다. 단, Linux 서버 버전을 기본 설치한 경우 이 기능이 기본으로 지원되지 않을 수 있으며, 별도로 검색해 추가 설치가 필요할 수 있다.

Shell이라는 용어 자체는 하나의 의미로 고정되지 않는다. 프로그램 그 자체를 가리킬 수도 있고, 역할의 관점에서 설명할 수도 있다. 어느 관점에서 보느냐에 따라 조금씩 다르게 받아들이면 된다.

Shell의 종류도 다양하다. Bourne shell, C shell, Korn shell, Z shell 등이 존재한다. CPU마다 내부에서 처리하는 machine language(기계어)가 다르듯, 쉘도 여러 종류가 있다. 이는 어떤 환경이나 도구든 시대와 요구에 따라 부족한 점이 생기고, 그것을 보완하기 위해 새로운 종류가 계속 등장하기 때문이다.

현재 사용 중인 쉘을 확인하려면 다음 명령어를 입력하면 된다.

echo $SHELL

echo는 뒤에 오는 내용을 화면에 출력하는 명령어이고, $는 뒤에 오는 이름이 변수(variable)임을 나타내며 그 변수에 저장된 값을 출력하라는 의미다. SHELL이 대문자인 이유는 environment variable(환경변수)이기 때문이며, 현재 로그인한 사용자가 사용 중인 shell의 이름을 저장하고 있다.


Shell의 시작

Shell은 로그인(login)과 함께 시작된다. 이를 login shell(로그인 쉘)이라고 한다. 현재 시스템에 어떤 shell이 설정되어 있는지 확인하려면 다음 명령어를 사용한다.

cat /etc/passwd

출력 결과를 보면 각 계정마다 마지막 필드에 shell 경로가 표시된다. /bin/bash라면 해당 사용자가 bash shell을 사용 중이라는 뜻이고, nologin이라면 login shell이 없다는 뜻이다. 사용자가 어떤 shell을 쓰는지 이 방식으로도 확인할 수 있다.

그렇다면 nologin은 왜 존재할까? Linux/Unix에서는 사람이 직접 사용하는 계정 외에도 서비스 운영을 위한 계정이 존재한다. 슬라이드의 예시에서 sshd는 SSH daemon(데몬)을 위한 계정인데, daemon은 백그라운드(background)에서 자동으로 동작하는 프로그램이므로 사람이 직접 로그인해 관여할 필요가 없다. 이런 계정에 login shell을 부여하지 않는 이유는 보안(security) 때문이다. 사용하지 않는 login shell이 존재하면 오히려 해킹(hacking)의 위험 요소가 될 수 있다. 잘 돌아가는 서비스에 굳이 불필요한 접근 경로를 열어둘 필요가 없다는 개념이다.

shell의 변경도 가능하다. 사용자 계정을 생성할 때 shell을 지정할 수 있으며, 이후에는 chsh 명령어로 현재 사용 중인 shell을 다른 shell로 변경할 수 있다.


Shell Prompt의 종류

Shell prompt(쉘 프롬프트)는 단순한 기호가 아니라 현재 어떤 권한(permission)으로 작업 중인지를 나타내는 중요한 지표다.

Bash shell 기준으로 일반 사용자(regular user)는 \(, Z shell 기준으로는 %가 표시된다. 반면 root 사용자, 즉 superuser는 #으로 표시된다. 슬라이드의 예시처럼 sudo su 명령어를 입력하면 일반 사용자 상태인 \)에서 root 상태인 #으로 전환되는 것을 확인할 수 있다.

이 구분이 중요한 이유는 권한 범위가 완전히 다르기 때문이다. # 상태, 즉 root 권한에서는 이미 superuser이므로 sudo를 별도로 사용할 필요가 없다. 반면 $% 상태에서 높은 권한이 필요한 작업을 수행하려면 sudo가 필요한 경우가 생긴다.

특히 # 상태에서는 각별한 주의가 필요하다. Root는 파일의 소유(ownership)나 권한(permission)에 관계없이 시스템의 모든 것을 처리할 수 있는, 말 그대로 무적 상태다. 예를 들어 rm 명령어를 무심코 실행하면 의도하지 않은 파일이 삭제될 수 있다. 자신이 일반 사용자라고 착각한 채 root 상태에서 작업하다 시스템에 치명적인 문제를 일으키는 경우가 실제로 발생한다. 따라서 프롬프트 기호를 항상 확인하는 습관이 필요하다.


Shell의 기본 동작

Shell은 기본적으로 입력되는 내용을 순서대로 처리한다. 단, 두 가지 예외가 있다. 첫 번째 예외는 주석문(comment)이다. Shell prompt에서 #는 root 사용자를 의미한다고 했지만, 명령을 입력하는 쪽에서 #가 등장하면 의미가 달라진다. 이때의 #는 주석문 기호로, 컴퓨터는 이 기호 이후의 내용을 무시한다. 사람이 코드에 설명을 덧붙이기 위해 사용하는 용도다. 기본 동작 과정은 다음 여섯 단계로 이루어진다.

1단계 - 입력(input)은 세 가지 경로로 이루어진다. 사용자가 키보드로 입력하는 terminal(터미널), 파일(file), 그리고 argument(인수)로 전달되는 문자열(string)이다. Terminal이라는 명칭은 초창기 컴퓨팅 환경에서 비롯된 역사적 용어이며, 지금은 키보드로 명령을 입력하는 창이라고 이해하면 된다.

2단계 - 입력의 구성으로, 입력은 word(단어)와 operator(연산자)로 이루어지며 특수문자(special character)에 의해 분리된다. 단어는 공백으로 구분되는 문자들의 나열이고, 연산자는 +, =, *, /와 같이 연산을 수행하는 기호를 말한다. Shell에서 이것이 어떻게 활용되는지는 차차 확인해본다.

3단계 - 명령의 분류로, 명령은 simple command(간단한 명령)와 compound command(복합 명령)로 나뉜다. ls처럼 단독으로 사용되는 것이 simple command이고, 여러 명령어가 결합되거나 괄호 등이 추가되는 형태가 compound command다.

4단계 - 확장(expansion) 처리로, 단순한 입력을 넘어 더 진화된 방식으로 처리하는 단계다. 구체적인 내용은 이후에 다룬다.

5단계 - redirection(방향 전환) 처리로, |, >, <와 같은 redirection operator(방향 전환 연산자)와 그 피연산자(operand)를 처리하고 제거하는 단계다.

6단계 - 명령 수행(execution)으로, 앞선 단계를 모두 거친 후 최종적으로 명령이 실행된다.


인용 처리 (Quoting)

Quoting(인용 처리)이란 shell에서 미리 지정된 특별한 의미를 무시하고, 문자를 있는 그대로 처리하도록 하는 것이다. Shell에서 인용 처리는 세 가지 방식으로 이루어진다.

첫 번째는 escape character(이스케이프 문자) (backslash)다. 백슬래시 바로 뒤에 오는 문자를 특수한 의미 없이 그대로 처리한다. 다른 방식들과 혼용될 수 있으므로 구분해서 이해해야 한다.

두 번째는 single quote '(홑따옴표)다. 홑따옴표로 둘러싸인 개별 문자들을 모두 그대로 처리한다.

세 번째는 double quote "(겹따옴표)다. 겹따옴표로 둘러싸인 모든 문자를 그대로 처리한다. 여기서 주의할 점이 있다. 문서나 보고서에서 흔히 보이는 " 또는 ' 형태의 열림·닫힘 따옴표(curly quote)는 shell에서 인식하지 못한다. Shell에서 유효한 따옴표는 키보드 Enter 키 옆에 있는 '와 ", 즉 straight quote뿐이다. 인터넷이나 문서에서 명령어를 복사해 붙여넣을 때 이 curly quote가 섞여 들어와 오류가 발생하는 경우가 실제로 종종 있으므로 주의해야 한다. Single quote와 double quote의 동작 방식은 원칙적으로 다르며, 요즘은 엄격하게 구분되지 않는 경우도 있지만 기본 원칙은 알아두는 것이 좋다.


예약어 (Reserved Words)

Reserved word(예약어)란 shell 내부적으로 이미 의미가 지정되어 있어, 변수 이름 등 다른 용도로 사용할 수 없는 단어들이다.

Shell의 예약어 목록은 다음과 같다.

if, then, elif, else, fi, time, for, in, until, while, do, done, case, esac, coproc, select, function, { }, [[ ]], !

여기서 눈여겨볼 것이 있다. if 문의 끝을 나타내는 것이 fi인데, 이는 if를 거꾸로 쓴 것이다. 마찬가지로 case 문의 끝을 나타내는 것은 esac으로, case를 거꾸로 쓴 것이다. C언어에서는 중괄호 { }로 블록의 시작과 끝을 구분하지만, shell은 이처럼 키워드를 거꾸로 쓰는 방식으로 구분한다.

for, while, until, select 등은 do로 시작해서 done으로 끝나는 방식으로, 뒤집기 방식이 아니라 별도의 키워드를 사용한다.


간단한 명령어와 Pipeline

간단한 명령어 (Simple Command)

Simple command(간단한 명령어)는 공백으로 구분되는 단어들이 나열되는 방식으로 사용된다. ls -l처럼 첫 번째 단어는 실행될 명령어로 인식되고, 나머지 단어들은 해당 명령어의 argument(인수)로 사용된다.

Pipeline

Pipeline(파이프라인)은 | 기호를 사용한다. 키보드에서 이 기호를 찾을 때 대문자 I, 숫자 1, 소문자 l과 혼동하지 않도록 주의해야 하며, 반드시 실습을 통해 직접 확인하는 것이 좋다.

Pipeline은 하나 또는 그 이상의 명령어가 | 또는 |&로 구분되어 나열되는 방식으로 사용된다. 핵심 개념은 앞 명령어의 실행 결과가 뒤 명령어의 입력으로 전달된다는 것이다.

||&의 차이는 다음과 같다.

  • | : 앞 명령어의 standard output(표준 출력)만 다음 명령어의 standard input(표준 입력)으로 전달된다. 즉, 실행 도중 발생하는 오류 메시지는 전달되지 않고 화면에만 출력된다.

  • |& : 앞 명령어의 standard output(표준 출력)과 standard error(표준 오류)를 모두 다음 명령어의 standard input으로 전달한다. 2>&1|과 동일한 의미다.

|&가 필요한 이유는, |만 사용할 경우 중간에 오류가 발생해도 다음 명령어는 이를 전혀 감지하지 못하기 때문이다. 오류 메시지까지 함께 처리해야 하는 상황에서는 |&를 사용한다.


명령어 목록 (Command List)

명령어 목록이란 ;, &&, ||로 연결되는 명령어들의 묶음이며, ;, &, 또는 newline 중 하나로 끝나기도 한다. 즉, A; B; C처럼 한 줄에 여러 명령어를 엔터키를 누르기 전에 나열하는 것이 가능하다.

각 구분자의 동작 방식은 다음과 같다.

& (background execution) 은 명령어 실행이 완료되기를 기다리지 않고 다음 순서를 진행한다. 앞서 shell의 기본 동작에서 언급했던 개념이 바로 이것이다. 예를 들어 sleep 100을 실행하면 100초 동안 프롬프트가 돌아오지 않아 사용자는 아무것도 할 수 없다. 그러나 sleep 100 &처럼 &를 붙이면 100초가 지나지 않아도 즉시 프롬프트가 돌아와 다음 작업을 계속할 수 있다. 이것이 background execution(백그라운드 실행)이다.

; 는 명령어 실행이 완료될 때까지 기다렸다가 다음 순서를 진행한다. a; b; c처럼 나열하면 a가 끝난 후 b, b가 끝난 후 c 순서로 실행된다.

&& || 도 마찬가지로 ; 대신 사용할 수 있으며, 각각 다른 조건으로 다음 명령어의 실행 여부를 결정한다. 이 부분은 직접 실습을 통해 확인하는 것이 가장 좋다.

한 가지 주의할 점은, 인터넷이나 AI 검색 결과로 나오는 명령어 예시가 실제로 동작하지 않는 경우가 있다는 것이다. 이를 직접 실행해보고 구분하는 과정 자체가 실력을 검증하고 향상시키는 과정이 된다. ; 사용 시 앞 명령어가 정상적으로 끝나지 않았을 때 어떻게 동작하는지도 직접 실습으로 확인해보자.


명령어 목록 - &&||

&가 두 개가 되면 &&, vertical bar가 두 개가 되면 ||가 된다. 각각의 동작 방식은 다음과 같다.

명령어1 && 명령어2 는 논리적으로 AND 연산에 해당한다. 명령어1이 성공, 즉 exit code(종료 코드) 0을 반환했을 때만 명령어2가 실행된다. 예를 들어 ls korea를 실행했을 때 korea라는 파일이 존재하면 정상적으로 실행되어 0을 반환하고 다음 명령어로 넘어간다. 반대로 오류가 발생해 0이 아닌 값을 반환하면 뒤의 명령어는 아예 실행되지 않는다.

명령어1 || 명령어2 는 논리적으로 OR 연산에 해당한다. 명령어1이 실패, 즉 0이 아닌 값을 반환했을 때만 명령어2가 실행된다. 반대로 명령어1이 성공하면 명령어2는 실행되지 않는다.

왜 이런 복잡한 구분이 필요할까? 바로 조건적 실행(conditional execution)이 필요하기 때문이다. 예를 들어 directory(디렉토리)를 생성한 후 그 안에 파일을 복사하는 작업을 한다고 가정하면, 목적지 directory가 정상적으로 만들어졌을 때만 파일 복사를 시도하는 것이 합리적이다. directory가 존재하지 않는 곳에 파일을 복사하려 하면 어차피 실패하기 때문이다. 이처럼 앞의 명령어가 성공했을 때만 뒤의 명령어를 실행해야 하는 상황에서 &&가 유용하게 쓰인다.


복합 명령어 (Compound Command)

복합 명령어는 프로그래밍 언어의 구조적 형태로 사용된다. 앞서 살펴본 예약어(reserved word) 또는 제어 연산자(control operator)로 시작하고, 상응하는 예약어 또는 연산자로 종료된다. if로 시작해 fi로 끝나는 것이 대표적인 예다.


복합명령어(Compound Command) ; until

until: until의 사용 방법은 다음과 같다.

until test-commands; do consequent-commands; done

여기서 ;의 위치에 주의해야 한다. ;가 없으면 문법 오류(syntax error)가 발생한다.

동작 방식은 다음과 같다. test-commands의 종료 상태값(exit code)이 0이 아닐 때 consequent-commands를 반복 실행한다. 이는 while과 반대 조건이다. 반환값(return value)의 경우, 실행된 명령이 없으면 0을 반환하고, 그렇지 않으면 consequent-commands의 마지막 명령의 실행 결과를 반환한다.

반환값은 단순한 작업에서는 크게 신경 쓰지 않아도 된다. 그러나 작업이 복잡해질수록 앞선 작업이 제대로 끝났는지 확인해야 하는 상황이 생긴다. &&||처럼 앞 명령어의 성공 여부에 따라 다음 명령어를 실행할 때도 이 반환값이 기준이 된다. 따라서 어떤 조건에서 반복이 이루어지는지, 그리고 반환값이 언제 참(0)이 되고 거짓(0이 아닌 값)이 되는지의 개념은 기억해두어야 한다. 구체적인 사용법은 실제로 사용할 때 manual(매뉴얼)을 참고하면 된다.

📌좀 더 쉬운 설명

count=1

until [ $count -gt 5 ]; do
    echo "현재 숫자: $count"
    count=$((count + 1))
done

실행 결과는 이렇다.

현재 숫자: 1
현재 숫자: 2
현재 숫자: 3
현재 숫자: 4
현재 숫자: 5

동작 방식을 풀어서 설명하면 이렇다.

  • count가 1부터 시작

  • [ $count -gt 5 ] : count가 5보다 큰가? 를 매번 확인

  • 처음에는 1이니까 5보다 크지 않다 → 실패 → 반복 실행

  • count가 6이 되는 순간 5보다 크다 → 성공 → 반복 종료

즉 조건이 실패하는 동안 계속 echocount 증가를 반복하다가, 조건이 성공하는 순간 멈추는 것이다.


복합명령어(Compound Command) ; while

until과 구조는 동일하지만 조건이 반대다.

  • until : test-commands의 exit code가 0이 아닐 때 반복

  • while : test-commands의 exit code가 0일 때 반복

즉 조건이 성공하는 동안 계속 반복하고, 실패하면 멈춘다.


복합명령어(Compound Command) ; for

for는 두 가지 형태로 사용된다.

첫 번째 형태

for name [[in words ...];] do commands; done

여기서 [ ]는 shell 문법에서 선택사항(optional)을 의미한다. man 명령어로 매뉴얼을 볼 때도 [ ] 안의 내용은 필요할 때만 쓰면 된다는 뜻이다. 따라서 in words 부분은 생략 가능하고, 그 뒤의 ;in words가 있을 때만 필요하다.

동작 방식은 words를 확장해 목록을 만들고, 목록의 각 word에 대해 commands를 1회씩 실행한다. 실행된 것이 없으면 0을 반환하고, 있으면 commands의 마지막 명령의 실행 결과를 반환한다.

예를 들면 이렇다.

for fruit in apple banana orange; do
    echo $fruit
done
apple
banana
orange

apple, banana, orange 각각에 대해 echo를 1회씩 실행한다. Python의 for 문과 동일한 방식으로 동작한다.


복합명령어(Compound Command) ; for

C언어의 for문과 동일한 개념이다.

// C언어
for (i=0; i<10; i++)

// Shell
for ((i=0; i<10; i++))

동작 순서는 다음과 같다.

  1. expr1 초기값 설정 (예: i=0)

  2. expr2 조건 확인 → 0이 아니면(참) commands 실행

  3. expr3 처리 (예: i++)

  4. 다시 2번으로 돌아가 반복

  5. expr2가 0(거짓)이 되면 반복 종료

반환값은 commands의 마지막 명령 실행 결과를 반환한다.


복합명령어(Compound Command) ; if

필수 요소와 선택 요소를 구분하면 이렇다.

  • 필수 : if, then, fi, 그리고 명령 끝의 ;

  • 선택 : elif, else ([ ]로 표시된 부분)

then이 없으면 문법 오류가 발생한다. 동작 방식은 test-commands를 실행해 exit code가 0(성공)이면 consequent-commands를 실행하고, 0이 아니면 elif 또는 else로 넘어간다.


복합명령어(Compound Command) ; case

case를 거꾸로 쓴 것이 esac으로 종료를 나타낸다. 문법에서 눈여겨볼 부분이 두 가지 있다.

첫 번째는 ((여는 괄호)다. [(]로 표시되어 있으므로 선택사항이다. 보통 case문에서 조건 패턴을 쓸 때 ) 닫는 괄호만 쓰는 것이 일반적이지만, 괄호는 열고 닫아야 짝이 맞는다고 생각한다면 (를 써도 된다. 둘 다 허용된다.

두 번째는 ;;(세미콜론 두 개)다. 일반적인 ;와 달리 case문에서는 각 패턴의 command-list 끝에 ;;를 사용한다. 이것이 해당 패턴의 실행이 끝났음을 나타낸다. 그리고 [| pattern]처럼 |를 사용해 패턴을 여러 개 나열할 수 있다. 즉 하나의 조건에 여러 패턴을 지정하는 것이 가능하다. 동작 방식은 word가 pattern과 일치할 때만 해당 command-list를 실행한다.

복합 명령어(Compound Command) - case 종료자의 종류

case문에서 각 패턴 블록의 끝에 오는 종료자(terminator)는 세 가지가 있으며, 각각 동작 방식이 다르다.

;; 는 처음 일치하는 pattern의 command-list만 실행하고 완전히 종료한다. 다른 언어의 case문 또는 switch문에서 break를 사용하는 것과 동일한 개념이다.

;& 는 처음 일치하는 pattern의 command-list를 실행하고, 다음 pattern의 일치 여부에 상관없이 그 다음 command-list도 강제로 실행한다. 다른 언어의 switch문에서 break 없이 아래로 흘러내려가는 fall-through와 동일한 개념이다.

;;& 는 처음 일치하는 pattern의 command-list를 실행한 후, 다음 pattern과 비교를 계속 진행한다. 일치하면 실행하고, 일치하지 않으면 건너뛰며, 끝까지 비교를 계속한다. 다른 언어의 continue와 유사한 개념이다.

정리하면 이렇다.

종료자 동작
;; 일치하는 것 하나만 실행하고 종료
;& 일치하는 것 실행 후 다음 것 무조건 실행
;;& 일치하는 것 실행 후 나머지 pattern 계속 비교

복합 명령어(Compound Command) - select

select name [in words ...]; do commands; done

for문과 구조가 거의 동일하다. words를 확장해 목록을 만들고, 각 word를 순서대로 name에 지정하며 commands를 실행한다. 앞서 예시로 든 apple, banana, orange라면 한 번은 apple, 한 번은 banana 값을 가지는 방식이다. for와의 차이는 select는 자동으로 번호가 매겨진 메뉴를 화면에 출력하고 사용자의 선택을 기다린다는 점이다.


복합 명령어(Compound Command) - (( ... )), [[ ... ]]

(( expression ))

수학적인 계산을 실행한다. +, -, *, / 같은 산술 연산(arithmetic operation)을 처리한다.

(( 1 + 2 ))

[[ expression ]]

조건문 평가를 실행한다. 예를 들어 a가 b보다 큰지, 값이 같은지 등의 조건을 판단한다.

여기서 헷갈리기 쉬운 부분이 있다. Shell에서 참/거짓의 반환값은 일반적인 프로그래밍 언어와 반대다.

Shell C, Python 등
참(true) 0 1
거짓(false) 0이 아닌 값 0

Shell에서 0이 성공, 즉 참을 의미하는 이유는 성공은 하나지만 실패의 종류는 여러 가지이기 때문이다. 실패했을 때 그 원인을 숫자로 구분하기 위해 이런 방식을 채택했다.


복합 명령어 - (...), !, &&, ||

앞서 명령어 나열에서 설명했던 &&||의 동작 방식이 여기서 논리 연산(logical operation)의 관점으로 연결된다. &&는 앞이 참일 때만 뒤를 실행해야 최종적으로 참인지 알 수 있고, ||는 앞이 실패했을 때 뒤를 실행해야 하나라도 참인지 확인할 수 있다.


명령어 그룹 만들기

명령어를 하나의 그룹으로 묶는 방법은 두 가지이며, 가장 큰 차이는 어디서 실행되느냐다.

(list) 는 subshell(서브 쉘) 환경에서 명령을 실행한다. Subshell에서 사용된 변수는 subshell이 종료되면 사라진다.

{ list; } 는 current shell(현재 쉘) 환경에서 명령을 실행한다.

이 차이를 변수의 관점으로 이해하면 쉽다. 변수 A=1을 저장했을 때를 생각해보자.

  • Subshell (...) : 자식이 자신의 세계에서 A=2로 바꿔도 부모 세계의 A는 여전히 1이다. Subshell 안에서 변경된 내용은 밖으로 나오지 않는다.

  • Current shell { } : 같은 세계에서 실행되므로 변경된 내용이 그대로 유지된다.

이는 이후에 다룰 fork(포크) 개념과 연결된다. Fork란 자식 process를 계속 생성하는 것인데, 부모와 자식은 각자의 세계를 가지므로 자식 세계에서의 변경이 부모 세계에 영향을 주지 않는다.


함수 (Function)

함수는 프로그래밍에서 코드의 재사용(reuse)을 위한 개념이다. 반복적으로 사용되는 코드를 하나의 처리 단위로 묶고 이름을 붙여, 필요할 때마다 그 이름만 호출해서 사용할 수 있도록 하는 것이 함수의 역할이다.

Shell에서 함수를 만드는 방법은 두 가지다.

# 첫 번째 방법
fname () compound-command [redirections]

# 두 번째 방법
function fname [()] compound-command [redirections]

두 방법의 차이는 function 키워드의 유무이다. function을 앞에 명시하면 이것이 함수임을 더 명확하게 나타낼 수 있다. 기능상의 차이는 없으므로 어느 방식을 사용해도 무방하다.

[ ]로 표시된 redirections는 선택사항이며, 함수 실행 결과를 어디로 보낼지 방향을 지정할 때 사용한다. 앞서 배운 redirection 개념이 함수에도 그대로 적용된다.


Shell Parameters (쉘 매개변수); 이름

name=[value]

변수를 선언하고 값을 저장하는 가장 기본적인 방법이다. [value]가 선택사항인 이유는 값 없이 변수 이름만 선언하는 것도 가능하기 때문이다. 즉 초기값 없이 선언만 해두고 나중에 값을 넣을 수 있다.


Shell Parameters (쉘 매개변수); 위치 (Positional Parameter)

\({N}  또는  \)N

위치(position)를 숫자로 가리키는 parameter다. 수직선상에 0, 1, 2처럼 위치가 있다고 생각하면 이해하기 쉽다.

예를 들어 ls /home을 입력했을 때를 보면 이렇다.

위치
$0 ls (명령어 자체)
$1 /home (첫 번째 argument)
$2 두 번째 argument

\(0은 실행된 명령어 이름이고, \)1부터가 사용자가 넘긴 argument다. 이것이 positional parameter(위치 매개변수)가 0이 아닌 위치를 가리킨다고 표현한 이유이며, shell script를 작성할 때 매우 자주 활용된다.


Shell Parameters - 특수 문자 *@

*@ 모두 1부터 시작하는 positional parameter(위치 매개변수)로 확장된다는 점은 동일하다. 그러나 따옴표로 감쌌을 때 동작 방식이 달라진다.

*

형태 동작
$* \(1 \)2 $3 ... 으로 확장
"$*" "\(1c\)2c$3..." 하나의 문자열로 합쳐짐

여기서 c는 IFS(Internal Field Separator)에 해당하는 구분 문자다. IFS는 필드를 구분하는 문자로, 기본값은 공백이지만 콤마(,) 등 다른 문자로 지정할 수도 있다. 엑셀에서 CSV(Comma-Separated Values) 파일로 저장할 때 콤마가 구분 문자가 되는 것과 같은 개념이다. "$*"로 감싸면 모든 값이 IFS 구분 문자로 이어진 하나의 문자열이 된다.

@

형태 동작
$@ \(1 \)2 $3 ... 으로 확장
"$@" "\(1" "\)2" "$3" ... 각각 독립된 문자열로 유지

@에는 c(IFS 구분 문자)가 없다. "$@"로 감싸면 각 값이 따옴표로 개별 보호되어 독립된 문자열로 유지된다.

결국 가장 큰 차이는 따옴표로 감쌌을 때다. "\(*"는 전체를 하나로 합치고, "\)@"는 각각을 개별적으로 유지한다.


Shell Parameters - 특수 문자 #, ?, -

#

$#

positional parameter의 개수를 십진수(decimal)로 반환한다. 즉 전달된 argument의 개수를 나타낸다. 예를 들면 이렇다.

명령어 $#
ls abc.txt 1
cp abc.txt def.txt 2

참고로 #은 context(문맥)에 따라 의미가 달라진다는 점을 기억해두자.

위치 의미
Shell prompt root 사용자
코드 내부 주석(comment)
$# argument 개수

?

$?

가장 최근에 실행된 명령어의 exit code(종료 상태값)로 확장된다. cp abc.txt def.txt가 정상적으로 종료되었다면 $?는 0을 반환한다. 앞서 &&||에서 다뤘던 exit code 개념이 여기서 그대로 적용된다.

-

$-

현재 shell에 설정된 option(옵션)의 상태로 확장된다. 구체적인 활용은 직접 실습을 통해 확인해보자.


Shell Parameters - 특수 문자 $, !, 0

Process와 Program의 차이

본격적인 내용에 앞서 process(프로세스)의 개념을 짚고 넘어간다.

  • Program(프로그램) : SSD와 같은 disk에 저장되어 있는 상태. 실행되지 않고 보관만 되어 있는 정적인 존재다.

  • Process(프로세스) : program이 실행되어 processor(프로세서) 안에서 활발하게 동작하고 있는 상태. 살아있는 동적인 존재다.

참고로 processor는 process를 처리하는 주체, 즉 CPU를 의미한다. -or이 붙으면 행위의 주체가 되듯이, processor는 process를 처리하는 것이다.

실행 중인 각 process에는 고유한 식별자인 PID(Process ID)가 번호로 부여된다. 현재 실행 중인 process와 PID는 ps 명령어로 확인할 수 있다.

$, !, 0

특수 문자 의미
$$ 현재 shell의 PID로 확장. subshell에서는 shell을 요청한 process의 PID
$! 가장 최근에 background로 실행한 process의 PID로 확장
$0 현재 실행 중인 shell 또는 shell script의 이름으로 확장

앞서 background 실행을 &로 한다고 배웠는데, $!는 그 background process의 PID를 가져올 때 사용한다.


2️⃣Shell Scripts

Shell은 지금까지 배워온 그 검은 명령창 환경 자체다. Shell은 단순한 프로그램이면서도 kernel과 사용자의 입출력을 연결해주는 역할을 한다. 지금까지 배운 내용을 정리하면 shell에서 명령을 사용하는 방법은 다양하다.

  • 명령어를 하나씩 입력하고 Enter

  • ;으로 여러 명령어를 한 줄에 연결

  • &&, ||로 조건부 실행

  • if, fi, for, while 등으로 조건문과 반복문 활용

그렇다면 이것들을 구체적으로 어떻게 활용할까? 바로 shell script(쉘 스크립트)다.

Shell script란 배우의 대본(script)과 같다. 배우가 대본에 적힌 대로 연기를 하듯, shell은 script에 적힌 명령들을 순서대로 읽고 실행한다. 즉 앞서 배운 모든 명령어, 조건문, 반복문을 하나의 파일에 담아 한꺼번에 실행할 수 있도록 만든 것이 shell script다.

모든 사람이 shell script를 작성할 일이 있는 것은 아니다. 그러나 개발자로서 이런 것이 존재한다는 것을 알고, 필요할 때 활용할 수 있다는 것을 인지하는 것만으로도 충분한 가치가 있다.


Shell Scripts란?

Shell script는 shell 명령어들을 포함하고 있는 text file(텍스트 파일)이다. 사람이 읽을 수 있는 형태의 문자들이 나열되어 있는 파일이다.

Shell Script 실행 방법

1) slash(/)가 없는 파일 이름의 경우 - 현재 directory에서 먼저 탐색

파일 이름이 단순히 a처럼 /가 없는 경우, shell은 가장 먼저 현재 작업 중인 directory(current working directory)에서 해당 파일을 찾는다. 현재 작업 directory는 pwd 명령어로 확인할 수 있으며, 로그인 직후에는 보통 home directory(/home/linux 또는 ~)가 된다.

반면 /a처럼 /가 붙으면 root directory 아래에서, /var/a처럼 경로가 지정되면 해당 경로에서 찾는다.

2) 현재 directory에 없으면 - $PATH에 지정된 순서로 탐색

현재 directory에서 찾지 못하면 $PATH environment variable(환경변수)에 지정된 경로들을 순서대로 탐색한다. 현재 설정된 PATH는 다음 명령어로 확인할 수 있다.

echo $PATH

PATH에 등록된 경로들이 순서대로 나열되며, shell은 이 순서대로 파일을 찾는다. 이 순서는 사용자 설정에 따라 달라질 수 있으며 절대적이지 않다.

3) 실행 권한(execute permission)의 유무

ls -al 명령어로 파일 권한을 확인했을 때 rwxx가 있느냐 없느냐가 실행 권한을 나타낸다. 권한은 소유자(owner), 소유자 그룹(group), 기타(others) 순서로 표시된다.

상황 실행 방법
실행 권한(x)이 있을 때 a 입력 후 Enter
실행 권한(x)이 없을 때 bash a 입력 후 Enter

실행 권한은 chmod 명령어로 부여할 수 있다. 권한이 없을 때 -로 표기된 자리에 x를 부여하면 파일 이름만으로 실행이 가능해진다.


Shell Script 첫 줄의 #! (Shebang)

Shell script 파일의 맨 첫 줄에 #!가 빈칸 없이 붙어 있는 경우, 이 줄은 해당 script를 해석할 interpreter(인터프리터)를 지정하는 역할을 한다. 이것을 shebang(쉬뱅)이라고 부른다.

#!/usr/bin/bash

/usr/bin/bash에 위치한 bash를 interpreter로 사용하겠다는 의미다. 경로를 명확하게 고정 지정하는 방식이다. 만약 이 줄이 없다면 현재 사용 중인 shell이 자동으로 script를 해석한다. 그러나 해당 경로에 bash가 존재하지 않으면 실행이 실패한다.

#!/usr/bin/env bash

env를 사용하면 고정된 경로 대신 $PATH에 등록된 경로들을 순서대로 탐색해 가장 먼저 찾아지는 bash interpreter를 사용한다.

두 방식의 차이를 정리하면 이렇다.

#!/usr/bin/bash #!/usr/bin/env bash
탐색 방식 고정 경로에서만 탐색 $PATH 전체에서 탐색
bash가 해당 경로에 없을 때 실행 실패 다른 경로에서 계속 탐색
이식성(portability) 낮음 높음

env를 사용하는 방식이 더 일반적이고 다양한 환경에서 동작할 가능성이 높다. 시스템마다 bash의 설치 경로가 다를 수 있기 때문에 #!/usr/bin/env bash가 더 안전한 선택이 될 수 있다.


Interactive Shell과 Non-Interactive Shell

Interactive Shell

Interactive(인터랙티브)란 말 그대로 사용자와 컴퓨터가 서로 주고받는 환경이다. 사용자가 입력하면 컴퓨터가 반응하고, 그 반응에 따라 사용자가 다시 입력하는 방식이다. 예를 들어 sudo apt install을 입력했을 때 설치 목록이 나오고 Y/N을 물어보는 것이 대표적인 interactive 환경이다.

기술적으로는 입력(input)과 오류 출력(error output) 모두 terminal에 연결되어 있는 상태이며, 사용자의 terminal을 통해 읽기와 쓰기가 진행된다.

Non-Interactive Shell

Non-interactive는 사용자가 명령을 실행한 후 추가적인 입력 없이 컴퓨터가 알아서 처리하는 환경이다. 실행 후 사용자가 개입할 일이 없는 상태다.

파일 또는 string(문자열)의 내용을 읽고 명령을 실행하는 방식으로 동작하며, shell script 파일을 실행할 때의 환경이 바로 non-interactive shell에 해당한다.

두 가지를 정리하면 이렇다.

Interactive Shell Non-Interactive Shell
사용자 개입 필요 불필요
입출력 terminal에 연결 파일 또는 문자열
대표적인 예 명령창에서 직접 입력 shell script 실행

환경 파일 (Environment Files)

Bash shell을 사용할 때 shell의 환경을 설정하는 파일들이 있다. prompt 모양, 현재 사용자 이름, 현재 directory 표시 방식, PATH 설정 등을 이 파일들에서 지정할 수 있다.

Login Shell 환경 파일 읽기 순서

Login shell이 시작될 때 다음 순서로 환경 파일을 읽고 실행한다.

  1. /etc/profile - 존재한다면 가장 먼저 읽음 (시스템 전체에 적용)

  2. ~/.bash_profile - 개인 home directory의 설정 파일

  3. ~/.bash_login

  4. ~/.profile

2, 3, 4번은 순서대로 탐색하며 존재하는 파일을 읽는다. ~는 본인의 home directory를 의미한다.

Logout 시 실행되는 파일

Interactive login shell이 종료되거나 non-interactive login shell의 실행이 끝나면 다음 파일을 읽고 실행한다.

~/.bash_logout

환경 파일 확인 방법

ls -al ~

위 명령어를 실행하면 .으로 시작하는 숨김 파일(hidden file)들이 표시된다. 이 중에 위에서 언급한 환경 파일들이 보일 것이다. 파일이 존재하지 않는 경우도 있는데, 이는 설치 버전의 차이가 아니라 계정 생성 방식에 따른 차이다. 없다면 직접 만들어서 사용하면 된다.


Bash 환경 파일 2

1) Interactive Non-Login Shell

처음에는 "interactive인데 왜 login이 없지?"라는 의문이 생길 수 있다. 이는 이미 login이 된 상태에서 새로운 shell을 열거나 terminal을 추가로 실행하는 경우를 말한다. 이미 인증이 된 상태이므로 login 과정 없이 interactive하게 사용할 수 있는 것이다. 이 경우 ~/.bashrc 파일이 존재하면 해당 파일을 읽고 실행한다.

2) su 명령어와 ~/.bashrc

처음 login한 다음 script를 실행하거나, - 옵션 없이 su 명령어를 사용하는 경우 환경 전체를 초기화하지 않고 ~/.bashrc만 실행한다.

명령어 동작
su - 환경 전체를 초기화하고 새로운 login shell 시작
su 환경을 초기화하지 않고 ~/.bashrc만 실행

3) ~/.bash_profile에 포함되는 일반적인 내용

if [ -f ~/.bashrc ]; then . ~/.bashrc; fi

이 구문의 의미는 "~/.bashrc 파일이 존재하면 실행하라"는 뜻이다. -f는 해당 파일이 존재하는지 확인하는 조건이다. 파일이 없는 상태에서 무조건 실행하라고 하면 오류 메시지가 발생하므로, 존재 여부를 먼저 확인하고 있을 때만 실행하는 것이다.

여기서 [ ]의 의미에 대해 짚고 넘어갈 필요가 있다. 앞서 매뉴얼 구문에서 [ ]는 optional을 의미했지만, 여기서의 [ ]는 전혀 다른 의미다. if문 안에서 조건을 판별하기 위한 expression(표현식)을 담는 조건문 기호다. 같은 기호라도 context(문맥)에 따라 의미가 달라진다는 점을 기억해두자.


Bourne Shell Variables - PS1, PS2

PS1 (Prompt String 1)

PS1은 shell prompt에 표시되는 내용을 지정하는 변수다. 예를 들어 로그인했을 때 보이는 linux@KOREACU:~$ 같은 형태가 PS1에 의해 결정된다.

PS1에서 사용할 수 있는 특수 문자들은 다음과 같다.

특수 문자 의미
\s shell의 이름
\v shell의 버전(version)
\$ superuser이면 #, 일반 사용자이면 $
\w home directory를 나타내는 ~pwd
\h 첫 번째 .까지의 hostname
\u 현재 사용자 이름

이 특수 문자들을 single quote(') 안에 조합해서 PS1을 설정하면 login 시 prompt에 해당 내용들이 표시된다.

PS2 (Prompt String 2)

PS2는 명령어가 여러 줄에 걸쳐 입력될 때 표시되는 secondary prompt다. 기본값은 >다.

주의사항

PS1에 너무 많은 정보를 담으면 prompt가 길어져 실제로 명령어를 입력할 수 있는 공간이 줄어든다. 따라서 꼭 필요한 정보만 선택해서 표시하는 것이 좋다.


스크립트 파일 .profile (일부)

ls -al ~ 명령어로 home directory의 숨김 파일들을 확인하면 .profile 파일을 볼 수 있다. 이 파일의 내용을 살펴보면 다음과 같은 구조로 되어 있다.

주석문

#으로 시작하는 줄은 주석문(comment)으로, 컴퓨터는 이 내용을 무시한다. 사람이 읽기 위한 설명을 적어두는 용도다.

조건문 구조

if [ -n "$BASH_VERSION" ]; then
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

여기서 사용된 조건 flag의 의미는 다음과 같다.

Flag 의미
-n string의 길이가 0이 아니면 참
-f 해당 이름이 존재하고 일반 파일(regular file)이면 참

동작 방식

첫 번째 조건 [ -n "\(BASH_VERSION" ]\)BASH_VERSION 변수에 값이 있는지 확인한다. 이 변수에 값이 있다는 것은 현재 bash shell을 사용 중이라는 의미다.

두 번째 조건 [ -f "$HOME/.bashrc" ]는 home directory에 .bashrc 파일이 존재하고 일반 파일인지 확인한다. -f에서 일반 파일(regular file)이라고 명시하는 이유는 같은 이름의 directory가 존재할 수도 있기 때문이다.

두 조건이 모두 참이면 . "$HOME/.bashrc"를 실행한다. 즉 bash shell을 사용 중이고 .bashrc 파일이 존재할 때만 해당 파일을 실행하는 안전한 구조다.


.profile - PATH 설정

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ]; then
    PATH="\(HOME/bin:\)PATH"
fi

첫 줄의 #은 주석문이므로 컴퓨터는 무시한다. 사람이 읽기 위한 설명이다.

조건문 -d

앞서 -f가 regular file 여부를 확인했다면, -d는 directory 여부를 확인한다.

Flag 의미
-f 일반 파일(regular file)이면 참
-d directory이면 참

동작 방식

home directory 안에 bin directory가 존재하면 PATH="\(HOME/bin:\)PATH"를 실행한다. 보통은 home directory 안에 bin이 없지만, 존재할 경우 PATH에 추가하는 것이다.

여기서 핵심은 순서다. \(HOME/bin이 앞에, 기존 \)PATH가 뒤에 오도록 콜론(:)으로 구분해 설정한다. PATH는 앞에서부터 순서대로 탐색하므로, 이렇게 설정하면 $HOME/bin을 기존 PATH보다 먼저 탐색하게 된다.

즉 같은 이름의 실행 파일이 여러 경로에 존재할 때, home directory의 bin에 있는 것을 우선적으로 사용하겠다는 의미다.


.bashrc (일부)

# if not running interactively, don't do anything 
case $- in
    *i*) ;;
    *) return;;
esac

첫 줄의 #은 주석문이다. "interactive로 실행 중이 아니라면 아무것도 하지 말아라"라는 설명이다.

구조 분석 : 앞서 배운 case문이 실제로 사용된 예시다. esac으로 끝나는 것도 확인할 수 있다. 그리고 패턴 앞에 (를 써도 되고 안 써도 된다고 했는데, 여기서는 생략된 형태로 사용되고 있다. $-는 앞서 배운 현재 shell의 option 상태를 나타내는 특수 변수다. 이 값 안에 i가 포함되어 있으면 interactive shell임을 의미한다.

순서가 중요한 이유 : 만약 *i**의 순서를 바꾸면 동작이 달라진다. *는 모든 경우에 해당되므로 먼저 오면 i가 포함된 경우도 모두 return을 실행해버린다. case문은 위에서부터 순서대로 패턴을 비교하므로, 구체적인 조건을 먼저, 포괄적인 조건을 나중에 써야 의도한 대로 동작한다.


.bashrc (일부) - chroot 설정

# set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
    debian_chroot=$(cat /etc/debian_chroot)
fi

첫 줄은 # 주석문으로 "현재 작업 중인 chroot를 식별하는 변수를 설정한다"는 설명이다.

새롭게 등장한 조건 flag

지금까지 배운 flag들을 정리하면 이렇다.

-n-z는 서로 반대 관계다. 동작 방식 두 조건을 &&로 연결했으므로 둘 다 참일 때만 실행된다.

첫 번째 조건 [ -z "${debian_chroot:-}" ] : debian_chroot 변수의 값이 비어있으면 참 두 번째 조건 [ -r /etc/debian_chroot ] : /etc/debian_chroot 파일이 존재하고 읽기 권한이 있으면 참

두 조건이 모두 참이면 debian_chroot=$(cat /etc/debian_chroot)를 실행한다. cat 명령어로 /etc/debian_chroot 파일의 내용을 읽어 debian_chroot 변수에 저장하는 것이다.

More from this blog

Linux/Unix Lecture Notes: Managing Processes and CPU Resources

리눅스/ 유닉스 시간의 교수님 녹취록입니다. 앞으로도 쭉 녹취록을 올릴것인데 모든 내용을 취합해서 블로그에 강의노트식으로 올릴예정입니다. 서술형으로 읽기 좋게 정리해주세요. 그리고 팩트체크도 해주세요. 추가설명은 필요없습니다. 테크니컬한 명사단어들과 제목은 영문으로도 표시해주세요. [] 1️⃣ 프로세스 개요(Overview of Process) 프로세스(P

May 10, 202628 min read
Linux/Unix Lecture Notes: Managing Processes and CPU Resources

My dev journey

138 posts