커맨드라인 사용법
따라하며 배우는 리눅스 명령어와 관습들

들어가며

리눅스나 맥 환경에서 프로그래밍이나 서버에 대해서 공부하기 시작하면 필연적으로 커맨드 라인 인터페이스를 활용하게 됩니다. 커맨드라인 인터페이스는 매우 강력한 환경이기도 하지만, 동시에 GUI에만 익숙한 경우 매우 생소한 작업 환경이기도 합니다. 하물며 커맨드라인 인터페이스는 당연한 듯이 시스템에 포함되어있고, 당연히 이 정도는 알고 있을 것이라고 가정하고 모든 이야기가 진행됩니다. 검색해보고 싶지만, 기호들이 섞여있어서 검색도 잘 안 됩니다. 😢 이런 건, 리눅스 서버 관리자만 알면 되는 거 아니냐고요? 아닙니다.

하지만 너무 걱정하시지는 말길 바랍니다. 처음하면 어려운 게 당연하니까요.

이 글에서는 커맨드라인을 처음 접하거나 익숙하지 않은 분들을 위해서, 커맨드라인에 대한 기초, 관습적으로 사용되는 표현이나 단축키, 그 외에 여러가지 팁들을 소개하고자합니다. 이 글에서는 리눅스 기초나 셸 스크립팅을 다루지 않습니다만, 필요하다면 약간의 설명을 해두었습니다. 부디 커맨드라인 위에서 살아남아 다시 만나기를 기원합니다. 🙏*

* 이 글은 맥OS(macOS)에서 작성되었으며 리눅스(Linux) 환경에도 대부분 그대로 적용 가능합니다. 윈도(Windows)에도 커맨드라인 인터페이스가 있습니다만, 사용 방법은 차이가 꽤 많이 납니다. WSL2를 사용하면 윈도에서도 리눅스 환경의 커맨드라인을 사용해볼 수 있습니다: WSL2(Windows Subsystem for Linux 2) 설치 및 사용 방법

44BITS 소식과 클라우드 뉴스를 전해드립니다. 지금 5,000명 이상의 구독자와 함께 하고 있습니다 📮

1. 커널, 셸, 그리고 터미널 프로그램이란?

커맨드라인 인터페이스에 대한 해설

먼저 커맨드라인에 뛰어들기에 앞서 핵심적인 개념들에 대해서 소개하고자합니다. 커맨드라인 인터페이스, 터미널, 셸 비슷비슷한 용어들이 혼용되어 사용되곤 합니다. 이 용어들에 대해 좀 더 정확히 구분을 해보는 것도 가능합니다. (사실 크게 구분하지 않아도 상관은 없습니다 😅)

먼저 커맨드라인 인터페이스는 오로지 문자열로만 이루어진 인터페이스를 의미합니다. 구체적인 실체가 없는 추상적인 단어로, 이 환경에서는 문자열을 출력하거나 문자열을 입력하는 것만이 가능합니다. 정말로 문자열만 사용 가능합니다. 요즘 컴퓨터에서 가장 기본이 되는 컬러풀한 고해상도 이미지 파일을 출력하는 건 불가능합니다.

터미널은 커맨드라인 인터페이스가 물리적으로 구현된 기계입니다. 리눅스나 맥에서 제공되는 터미널 애플리케이션은 정확히는 가상 터미널 기계입니다. 이 기계는 오직 문자열을 출력하고, 문자열만 입력받을 수 있습니다. 커맨드라인 인터페이스가 개념이라면, 터미널은 커맨드라인 인터페이스를 사용하는(혹은 구현하는) 물리적/가상적 기계라고 할 수 있습니다. 가상 터미널도 여러가지 프로그램이 있습니다. 맥OSmacOS의 경우 기본 프로그램은 터미널이지만, 개발자들은 다양한 기능을 제공하는 iTerm2를 더 선호하는 편입니다.

은 커맨드라인 인터페이스로 구현된 터미널에서 실행가능한 대화형 프로그램입니다. “컴퓨터를 사용한다”는 것은 좀 더 정확히 표현하면 어떤 입력을 처리하기 위해 프로그램에서 운영체제 커널에 명령을 보내고 커널에서는 물리적 컴퓨터 장비의 연산 능력을 사용해 계산한 결과를 다시 출력해주는 일련의 과정입니다. 셸은 커맨드라인 인터페이스로 운영체제 커널에 명령을 내릴 수 있는 인터렉티브 프로그램입니다.

셸은 REPL이라고도 이야기됩니다. REPL은 Read-eval-print loop의 줄임말로 셸이 동작하는 방식에 대해서 잘 드러내줍니다. 먼저 사용자가 한 줄의 명령어(텍스트)를 입력하고 엔터를 누르면, 이 내용을 셸이 읽어들입니다(read). 그리고 명령어를 해석해서 실행하고(eval), 실행된 결과를 출력(print)합니다. 셸은 이 과정을 반복(loop)합니다. 셸 위에서 사용자가 할 일은 “명령어”를 입력하는 것밖에 없습니다. 아직 어색할지도 모르지만, 생각보다 간단하죠?

가상 터미널을 실행하면 일반적으로 셸이 바로 실행됩니다. 이 환경은 커맨드라인 인터페이스입니다. 커맨드라인 인터페이스, 터미널, 셸은 구분이 되는 단어입니다만, 실제로는 이러한 이유로 구분없이 사용하는 경우가 더 많습니다. 이 글에서는 편의상 셸이라는 단어를 주로 사용하겠습니다.

커맨드라인 인터페이스, 셸, 터미널에 대한 더 자세한 내용은 다음 글을 참고해주세요.

노트
앱, 애플리케이션, 프로그램, 프로세스

앱, 애플리케이션, 프로그램은 사실 상 같은 의미로 사용됩니다. 편의상 이 글에서는 GUI의 경우 애플리케이션, CLI의 경우 프로그램이라는 단어를 선호하고 있지만, 아주 엄밀하게 구분하는 것은 아닙니다. 이러한 프로그램이 시스템상에서 실행중인 경우 프로세스라고 부릅니다. GUI 애플리케이션은 실행과 종료가 사용자에 의해서 명시적인 액션으로 이루어지지만, CLI 프로그램들은 프로세스가 실행되고 할 일을 마치자마자 종료되는 경우가 많습니다. 하지만 사용자 입력이나 외부 요청을 처리하기 위해 장시간 프로세스로 실행되어있는 경우도 있습니다.

프로그램과 프로세스는 구분이 가능하지만, 아주 엄밀하게 구분해야하는 맥락이 아닌 경우에는 프로그램을 넓은 의미에서 프로세스를 포함한 의미로 사용하기도 합니다.

2. 현재 디렉터리를 알려주는: pwd

터미널을 실행해봅니다. 우리는 커맨드라인 세상의 입구, 셸에 내동댕이 쳐졌습니다. “나는 누구인가? 여긴 어디인가?”부터 파악해볼 필요가 있겠죠? pwd는 현재 디렉터리를 출력해주는 명령어입니다.

$ pwd
/Users/44bits

셸에는 현재 디렉터리라는 개념이 있습니다. 윈도의 탐색기나, 맥의 파인더에서 특정 폴더를 열어놓은 것과 같다고 생각해보면 간단합니다. 셸을 실행하면 일반적으로 사용자 디렉터리에서 시작합니다. 맥이라면 /Users/<USER_NAME>, 리눅스라면 일반적으로 /home/<USER_NAME>이 됩니다. <USER_NAME>을 컴퓨터 계정의 이름으로 치환하면 됩니다.

맥이나 리눅스에서는 디렉터리 구분자로 /를 사용합니다. 가장 기본적인 개념이니 꼭 기억해두시기 바랍니다.

노트
명령어 입력과 실행 결과를 표현하는 방법

다시 pwd를 실행한 내용을 살펴보겠습니다. 44BITS에서는 일반적으로 아래와 같은 형식으로 커맨드라인 명령어 실행 결과를 보여드리고 있습니다.

$ pwd
/Users/44bits

여기서 사용자가 실제로 입력하는 내용은 pwd입니다. 그리고 엔터를 칩니다.

$은 셸의 프롬프트를 의미합니다. 이 문자열 뒤로, 한 줄이 입력 라인이라는 것을 나타냅니다. 즉, 이 부분이 사용자가 직접 입력해야할 내용입니다. 프롬프트가 꼭 $인 것은 아닙니다만, 글을 쓸 때는 관습적으로 $를 사용합니다.

그 다음 줄은 $로 시작하지 않습니다. 이 라인은 출력 결과를 의미합니다. pwd를 실행한 결과를 컴퓨터가 출력한 내용입니다. 같은 명령어를 실행하더라도 출력 결과는 시스템에 따라서 다를 수 있습니다. 예를 들어 /Users/44bits에서 44bits는 현재 사용중인 계정명이기 때문에, 여러분의 환경에서 실제로 실행해보면 결과가 다를 것입니다. 출력 결과는 상황에 따라 조금 가다듬거나 생략하는 경우도 있습니다.

#는 root 계정의 셸 프롬프트로 사용되기도 합니다. 하지만 동시에 #은 셸에서 주석을 의미하기 때문에 주석 용도로 사용되기도 합니다. 맥락에 따라서 적절히 해석할 필요가 있습니다.

3. 디렉터리 구조의 이해와 디렉터리 이동하기(cd)

디렉터릭 구조는 매우 심플합니다. 디렉터리는 최상위 루트 디렉터리 /부터 시작합니다. 디렉터리에는 디렉터리나 파일이 포함되어있습니다. /Users/44bits는 루트 디렉터리(/)에 속한 Users 디렉터리에 속한 44bits 디렉터리를 의미합니다. 루트 디렉터리로 이동을 해보겠습니다.

디렉터리를 이동할 때는 cd 명령어를 사용합니다. 앞에서는 pwd라는 하나의 완성된 명령어를 입력했습니다만, cd 명령어에는 인자가 필요합니다. cd <DIR> 형식으로 명령어를 입력해야합니다. 이 때 명령어와 인자 사이를 스페이스로 한 칸 띄워줍니다. <DIR>에는 이동하고자 하는 위치를 지정합니다.

$ cd /

아무것도 출력되지 않습니다만, 다시 pwd로 현재 위치를 확인해볼 수 있습니다.

$ pwd
/

출력 결과가 달라진 것을 알 수 있습니다. 즉, 명령어를 실행하는 위치가 달라졌습니다. 다시 원래 디렉터리로 이동해봅니다.

$ pwd
/
$ cd /Users/44bits
$ pwd
/Users/44bits

정상적으로 원래 디렉터리로 돌아왔습니다.

4. 현재 디렉터리 표현법, 절대 경로와 상대경로 이해하기

셸에서는 현재 디렉터리를 .(마침표)로 나타냅니다. 현재 디렉터리에서 현재 디렉터리로 이동해보겠습니다.

$ pwd
/Users/44bits
$ cd .
$ pwd
/Users/44bits

/(루트 디렉터리)로 시작하는 경로명을 절대 경로라고 부릅니다. 절대경로는 루트 디렉터리로부터 목적지까지 다다르는 모든 경로를 나타냅니다. /Users/44bits/ 아래의 Users 아래의 44bits 디렉터리를 의미 합니다. .은 이와 달리 상대 경로를 나타낼 때 주로 사용됩니다. 예를 들어 현재 디렉터리가 /Users 디렉터리일 때 /Users/44bits의 상대경로는 ./44bits라고 표현합니다.

$ cd /Users
$ pwd
/Users
$ cd ./44bits
$ pwd
/Users/44bits

일반적으로 ./를 생략해도 상대 경로를 의미합니다. 즉 ./44bits44bits는 같습니다. 예제들을 보다보면 ./을 생략해도 될 때도 굳이 붙이는 경우가 있는데, 그냥 두 표현이 같다고 생각하시는 게 좋습니다.

$ cd /Users
$ pwd
/Users
$ cd 44bits
$ pwd
/Users/44bits

상대경로를 의미하는 ./44bits44bits는 같은 의미를 가지지만, 절대 경로로 표현된 /44bits는 완전히 다른 의미를 가집니다. 이 차이가 이해되셨다면 충분합니다. 여기서 알 수 있는 사실은 상대 경로는 일반적으로 내가 어느 디렉터리에 있는지 알 때 자주 사용한다는 점입니다. 정확한 위치로 이동하고 싶은 경우에는 상대 경로보다는 절대 경로를 사용하는 것이 편리합니다.

5. 상위 디렉터리, 홈 디렉터리, 임시 디렉터리 /tmp

.과 비슷한 표현으로는 ..이 있습니다. ..은 상위 디렉터리를 의미합니다.

$ pwd
/Users/44bits
$ cd ..
$ pwd
/Users

..을 여러번 사용하는 것도 가능합니다. 상위 디렉터리의 상위 디렉터리는 ../..과 같이 표현합니다.

$ pwd
/Users/44bits
$ cd ../..
$ pwd
/

상대 디렉터리는 말그대로 상대적인 개념이므로 논리적으로 확장시켜나갈 수도 있습니다. 예를 들어 현재 디렉터리(/Users/44bits)의 상위 디렉터리 아래의 44bits 디렉터리의 상위 디렉터리의 상위 디렉터리 아래의 Users 디렉터리의 상위 디렉터리의 Users 디렉터리 아래의 44bits 디렉터리는 어디일까요?

$ pwd
/Users/44bits
$ cd ../44bits/../../Users/../Users/44bits
$ pwd
/Users/44bits

정답은 처음 현재 디렉터리와 같은 곳입니다! 😅

또 하나 자주 사용되는 관습 중 하나는 ~입니다. 어떤 의미인지 짐작이 가시나요? 정답은 바로 홈 디렉터리입니다. 리눅스나 macOS는 기본적으로 다중 사용자 환경을 가정하고 있습니다. 앞에서 언급했습니다만, macOS의 경우 /Users/ 아래에, 리눅스라면 /home/ 아래에 사용자 디렉터리가 생성됩니다. macOS에서 44bits라는 계정을 가진 사용자의 홈 디렉터리 절대 경로는 /Users/44bits가 됩니다.

여기서 잠깐 현재 사용자를 확인하는 명령어 whoami를 실행해보겠습니다.

$ whoami
44bits

~(틸데)는 현재 셸 프로세스를 실행한 사용자의 홈 디렉터리를 의미합니다. 즉, ~/Users/44bits와 같습니다. 어디에 있건 ~는 현재 사용자의 홈 디렉터리를 의미합니다. cd~로 간단히 확인해보겠습니다.

$ cd /
$ cd ~
$ pwd
/Users/44bits
$ cd /Users
$ cd ~
$ pwd
/Users/44bits

처음 보면 ~가 홈 디렉터리라는 걸 유추하기는 힘듭니다만, 당연하게 사용되는 관습 중 하나입니다. ~를 확장한 표현법으로는 ~<USERNAME>이 있습니다. ~ 바로 뒤에 사용자 이름을 붙이면 해당 사용자의 홈 디렉터리를 의미합니다. 현재 시스템에 44bits와 nacyot 사용자가 있다고 가정해보고, 이 표현법을 사용해보겠습니다.

$ cd ~44bits
$ pwd
/Users/44bits
$ cd ~nacyot
$ pwd
/Users/nacyot

여기까지 홈 디렉터리로 이동하는 3가지 /Users/44bits, ~, ~44bits 방법에 대해서 배웠습니다. 한 가지 방법을 더 소개하겠습니다. cd는 일반적으로 이동하고자 하는 디렉터리를 첫 번째 인자로 받습니다만, 인자 없이 실행하는 것도 가능합니다. 인자 없이 실행하면, 홈 디렉터리로 바로 이동합니다. 😮

$ cd /
$ cd
$ pwd
/Users/44bits
$ cd /Users
$ cd
$ pwd
/Users/44bits

사용자 권한으로 작업을 할 때는 일반적으로 홈 디렉터리를 사용합니다.

관리자 권한이 아닌 경우에는 루트 아래의 다른 디렉터리에 접근이 불가능한 경우도 있습니다만, 임시로 자주 사용되는 디렉터리 중 하나가 /tmp이니 참고해주세요. 단, /tmp에 저장한 작업 내용은 재부팅하면 사라질 수 있으니 주의가 필요합니다.

6. 디렉터리의 정보 파악: ls

셸에서 하는 가장 기본적인 작업은 디렉터리를 이동하고, 명령어를 실행하는 일입니다. 따라서 현재 어느 디렉터리에 있는지를 파악하고, 명령어를 실행하기 위해 적절한 디렉터리를 이동하고, 디렉터리의 정보를 얻는 것은 필수적인 작업입니다. 이거 하나면 된다고 할 정도로 많이 쓰이는 명령어가 바로 ls입니다.

/etc 디렉터리로 이동 후 ls를 실행해봅니다.*

* /etc는 주로 시스템과 관련된 설정 파일들이 모여있는 위치입니다.

/etc 디렉터리에서 ls를 실행한 결과

많은 파일과 디렉터리가 있는 것을 확인할 수 있습니다. 하지만 기본 Bash 셸의 출력으로는 모두 흰색 글씨로 출력되서 디렉터리와 구분이 되지 않습니다.

-G 옵션을 사용하면 타입에 따라서 다른 색으로 출력해줍니다.

ls에 -G 옵션으로 컬러를 입혀보았습니다

여기서 흰색 출력은 파일, 하늘색 출력은 디렉터리를 의미합니다. 보라색으로 출력된 내용도 있습니다만 이 출력은 다른 곳에 연결되어있는 파일을 의미합니다. 우선은 파일이라고 생각해도 무방합니다.

ls -d */ 명령어로 디렉터리만 출력해볼 수 있습니다.

ls로 디렉터리 목록만 출력한 결과

여기서 -dls에 사용할 수 있는 옵션입니다. */는 디렉터리만 찾기 위한 패턴입니다. *는 모든 문자에 매칭되는 특별한 의미를 가집니다. 즉 *//로 끝나는 모든 파일을 의미하는데, 디렉터리는 /로 끝나므로 현재 디렉터리에 있는 모든 디렉터리를 의미합니다.* 즉, 이 디렉터리들에 다시 cd로 이동하는 것이 가능합니다.

* 약간 설명이 부족한 부분이 있습니다만 ls -d */는 디렉터리만 출력할 때 사용하는 관습적인 명령어로 이해해도 무방합니다.

이외에 많이 사용하는 옵션은 -l입니다. -l(long) 옵션을 사용하면 파일과 디렉터리들에 대한 훨씬 더 자세한 정보를 보여줍니다. ls -l -G를 실행해봅니다.

ls에 -l(long) 옵션과 -G(컬러) 옵션을 함께 사용한 결과

여기에는 파일의 권한, 소유자, 그룹, 파일 크기, 수정일과 같은 정보들이 출력됩니다.*

* 이 글에서는 리눅스/맥OS의 파일 퍼미션이나 사용자 관리에 대해서는 다루지 않습니다. 당장은 크게 필요하지 않습니다만, 이에 대한 더 자세한 정보는 리눅스 기초를 다루는 문서들에서 찾아볼 수 있습니다.

출력 결과에서 눈치채셨을 수도 있지만, 출력 순서는 파일, 디렉터리 무관하게 파일 이름의 알파벳 순서입니다. 출력 결과를 정렬하는 몇 가지 옵션에 대해서 알아보겠습니다. -t는 가장 최근에 수정된 파일 순서대로 정렬해줍니다. -S는 사이즈가 큰 파일부터 출력해줍니다. -r 옵션을 붙이면 순서를 거꾸로 출력해줍니다.

다음은 ls -l -S -G -r을 실행한 결과입니다. 출력 결과와 옵션을 보고 어떤 의미인지 직접 해석해보시기 바랍니다.

ls -l -S -G -r를 실행한 결과

그 외에 한 줄에 결과를 하나씩 출력하는 -1 옵션이나, .으로 시작하는 파일도 모두 출력하는 -a가 많이 사용됩니다.

7. 파일, 디렉터리 조작을 위한 기본 명령어들

이번에는 파일과 디렉터리 조작을 위한 기본적인 명령어들 몇 가지를 소개해보고자 합니다. 먼저 작업을 위해서 홈 디렉터리로 이동해서 work 디렉터리를 하나 만들어보겠습니다. 홈디렉터리 표기법으로 나타내면 ~/work/가 됩니다. mkdir 명령어를 사용합니다.

$ cd ~
$ mkdir work
$ cd work
$ pwd
/Users/44bits/work
$ ls

mkdir은 현재 디렉터리에서 인자로 넘겨받은 이름으로 디렉터리를 만들어줍니다. ls도 실행해봤지만, 파일이나 디렉터리가 없기 때문에 아무 내용도 출력되지 않습니다. 그럼 현재 디렉터리에서 hello/world 디렉터리를 만들 수 있을까요? 한 번 시도해보겠습니다.

$ mkdir hello/world
mkdir: hello: No such file or directory

아쉽지만 명령어는 실패합니다. 이는 /가 디렉터리 깊이를 구분하는 특수한 의미를 가지고 있기 때문입니다. 즉 hello/world라는 이름을 가진 단일 디렉터리는 존재할 수 없습니다. 따라서 mkdirhello 디렉터리 아래에 world 디렉터리를 만들려고 시도했으나 hello 디렉터리가 없기 때문에 실패했습니다. 차례대로 hello 디렉터리를 만들고, 다시 cd로 이동한 후 world 디렉터리를 만들 수도 있습니다만, -p 옵션을 사용해서 한꺼번에 디렉터리를 만들 수 있습니다.

$ mkdir -p hello/world

ls-R 옵션을 붙여 재귀적으로 현재 디렉터리 아래의 내용을 모두 확인해볼 수 있습니다.

$ ls -R
hello

./hello:
world

./hello/world:

현재 디렉터리(.)에 hello 디렉터리가 있고, ./hello 디렉터리 아래에 world 디렉터리가 있고, ./hello/world 디렉터리 아래에 아무것도 없는 것을 확인할 수 있습니다.

이번에는 파일을 만들어보겠습니다. 파일을 만드는 가장 쉬운 방법은 touch 명령어를 사용하는 것입니다. 첫 번째 인자로 파일 이름을 지정하면 됩니다.

touch를 사용하면 빈 파일을 만들 수 있습니다

touch의 원래 용도는 (이미 존재하는 파일의 )파일의 접근 시간 혹은 수정 시간을 변경하는 일입니다만, 빈 파일을 만드는 용도로도 많이 사용합니다. 여기서 만들어진 a 파일은 비어있습니다. touch로 여러 개 파일을 한 꺼번에 만드는 것도 가능합니다.

$ touch b c d e
$ ls
a   b   c   d   e   hello

mv는 파일 이름 이동, cp는 파일 복사에 사용합니다. 각각 다음과 같은 형식으로 사용됩니다.

cp <SRC> <DEST>
mv <SRC> <DEST>

여기서 <SRC>는 복사/이동하고자 하는 파일의 경로, <DEST>는 새로운 경로입니다. a 파일을 airport로 이름을 바꿔보고, b 파일을 복사해 batman 파일을 만들어보겠습니다.

$ mv a airport
$ ls
airport b   c   d   e   hello
$ cp b batman
$ ls
airport b   batman  c   d   e   hello

파일 이동이나 파일 복사가 어떻게 이루어지는지 확인해볼 수 있습니다. 다른 디렉터리로 파일을 이동하는 것도 가능합니다.

$ mv airport hello/

간단한 명령어입니다만, 이 명령을 해석하려고 보면 생각보다 어렵습니다. mv에서 <SRC> 위치에 온 airport는 이동하고자 하는 파일 명입니다. 그런데 두 번째 인자 <DEST>hello/입니다. hello라고 해도 됩니다만, 디렉터리라는 컨텍스트를 명확하게 하기 위해서 뒤에 /를 붙여주었습니다. 이 명령어를 실행하면 airport 파일이 같은 이름을 유지하면서 hello/ 디렉터리 아래로 이동합니다. 여기서 중요한 게 같은 이름을 유지한다는 점입니다.

즉, mv airport hello/mv airport hello/airport와 같습니다. 인자에 파일이 오는지 디렉터리가 오는지에 따라서 미묘하게 해석이 달라진다는 걸 이해하는 게 중요합니다.

$ ls
b   batman  c   d   e   hello
$ cd hello
$ ls
airport world

그렇다면 이 파일을 다시 상위 디렉터리로 옮기려면 어떻게 해야할까요? 절대 경로를 지정할 수도 있습니다만(/Users/44bits/work), 앞에서 배운 ../를 사용하면 더 간단합니다.

$ mv airport ../
$ cd ..
$ ls
airport b   batman  c   d   e   hello

벌써 mkdir, touch, cp, mv 4개의 명령어를 배웠습니다. 이번에는 파일과 디렉터리를 삭제하는 rmrmdir을 배워보겠습니다. 아, 저는 rmdir은 거의 사용하지 않아서, rm만 설명하겠습니다.

rm을 사용하면 파일이나 디렉터리를 지울 수 있습니다. 먼저 c, d를 지워보겠습니다.

$ rm c d
$ ls
airport b   e   hello

rm에 인자로 삭제하고 싶은 파일들을 지정해주기만 하면 됩니다. 이번에는 hello/를 디렉터리를 삭제해볼까요?

$ rm hello/
rm: hello: is a directory

앗, 디렉터리는 rm으로 삭제할 수 없나보네요. 이 때는 -r -f 옵션을 사용할 수 있습니다.* 이 옵션을 사용하면 디렉터리와 그 아래에 있는 모든 내용을 삭제할 수 있습니다.

* 일반적으로 rm -r -f로 쓰기보다는 rm -rf와 같은 형식으로 쓰입니다. 이는 옵션을 지정하는 방식의 차이인데 결과는 같습니다. 이에 대해서는 뒤에서 더 자세히 설명합니다.

중요
rm -rf /

윈도나 맥OS를 사용하는 분들이라면 휴지통 개념에 익숙하실 겁니다. GUI 상에서 파일을 삭제하더라도 바로 삭제되지 않고, 보통은 휴지통으로 이동이 됩니다. 그리고 명시적으로 휴지통을 비워줘야만 파일이 정말로 삭제됩니다.

이와 달리 커맨드라인에서 rm으로 파일을 삭제하면 바로 삭제됩니다. 기본적으로 되돌릴 수 있는 방법은 없습니다. rm은 매우 위험한 명령어이므로 사용할 때 반드시 삭제 대상을 두 번, 세 번 다시 확인하는 것을 권장합니다. rm으로 실수로 중요한 파일을 날리는 건 시스템 관리자들이 흔히 겪는 실수입니다. 만약 실수로 rm -rf ~라는 명령어를 실행하는 날에는 컴퓨터에서 본인이 작업하던 내용들을 통째로 날려버릴 수 있습니다. 실수를 직감했다면 ^c를 마구 연타해주세요.

프로그래머나 시스템 관리자들 사이에서는 rm -rf /라는 명령어가 농담처럼 회자되곤 합니다. 이 명령어의 의미를 생각해보면 아찔해질 것 입니다. 사실 이 명령어는 대부분 권한 문제로 제대로 실행되지 않습니다. 하지만 일부라도 시스템과 관련된 파일이 삭제되거나, 관리자 권한으로 실행되는 경우 대재앙을 맞이하게 될 것입니다.

8. 문자열 출력, 파일과 관련된 셸 기본 명령어 모음

기본적인 파일 조작에 대해서 알아보았으니 이번에는 문자열과 파일 내용을 출력하는 방법에 대해서 알아보겠습니다. 가장 기본이 되는 명령어는 echo입니다. echo는 인자를 받아서 화면에 출력해줍니다.

$ echo Hello, world!
Hello, world!

커맨드라인 인터페이스의 가장 기본이 문자열의 입력과 출력이라는 점에서 가장 기본이 되는 명령어입니다. 하지만 단순히 입력한대로 출력하는 기능은 셸에서는 크게 필요가 없긴합니다. 😓*

* echo를 독립적으로 쓸 일은 많지 않습니다. 실제로 이걸 어디에 쓰는 건가 싶을 수도 있지만, 커맨드라인에서 동작하는 프로그램을 작성할 때는 화면에 문자열을 출력하는 게 가장 기본적인 기능입니다.

echo는 셸 스크립트에서 사용하거나, 환경변수 출력에 사용할 수 있습니다. 아직 환경변수가 무엇인지 설명하지는 않았습니다만 다음과 같이 실행해보면, 입력과는 조금 다른 출력 결과를 확인할 수 있습니다.

$ echo My home directory is $HOME.
My home directory is /Users/44bits.

$HOME/Users/44bits로 치환된 것을 알 수 있습니다. 환경변수에 대해서는 뒤에서 다시 이야기하도록 하겠습니다.

다음으로는 파일 내용을 출력하는 cat, head, tail 명령어에 대해서 알아보겠습니다. 가장 기본이 되는 명령어는 cat입니다. /etc로 이동해서 bashrc의 내용을 출력해보겠습니다.* ls에 인자를 넘겨주면 해당 패턴과 같은 파일만 확인하는 것이 가능합니다. 파일이 있는 것을 확인하고 cat으로 파일 내용을 출력해봅니다.**

* bashrc 파일이 없다면 bash.bashrc 파일을 출력해봅니다. 둘 다 없다면 ls를 실행해서 출력된 다른 아무 파일이나 출력해봅니다.

** cat을 인자 없이 실행하면 입력을 받는 상태가 되므로, 셸에 명령을 입력할 수 없습니다. 아직 소개하지는 않았지만 이런 경우 컨트롤 + C를 입력해 cat을 중지시킬 수 있습니다.

$ cd /etc
$ ls bashrc
bashrc
$ cat bashrc
# System-wide .bashrc file for interactive bash(1) shells.
if [ -z "$PS1" ]; then
   return
fi

PS1='\h:\W \u\$ '
# Make bash check its window size after a process completes
shopt -s checkwinsize

[ -r "/etc/bashrc_$TERM_PROGRAM" ] && . "/etc/bashrc_$TERM_PROGRAM"

cat은 항상 파일의 전체 내용을 출력해줍니다. head는 파일의 앞부분 일부를 출력해주고, 반대로 tail은 뒤에서 일부분을 출력해줍니다. 기본적으로 10줄을 출력해주고 -n <N> 옵션을 사용해 원하는 줄수만큼 출력할 수 있습니다. 여기서 옵션을 사용하는 새로운 패턴이 등장하는데, -n 옵션은 출력할 줄수 <N>을 인자로 받습니다. 따라서 앞부분 두 줄을 출력하려면 head -n 2 bashrc와 같이 사용합니다. 이 때 -n 2-n2로도 쓸 수 있습니다.

$ head -n2 bashrc
# System-wide .bashrc file for interactive bash(1) shells.
if [ -z "$PS1" ]; then

$ head -n 2 bashrc
# System-wide .bashrc file for interactive bash(1) shells.
if [ -z "$PS1" ]; then

반대로 끝에서 2줄을 출력하려면 tail을 사용하면 됩니다.

$ tail -n 2 bashrc

[ -r "/etc/bashrc_$TERM_PROGRAM" ] && . "/etc/bashrc_$TERM_PROGRAM"

의도한 대로 2줄만 출력되는 것을 확인할 수 있습니다.

노트
tail로 로그 파일 업데이트 확인하기

tail-f 옵션과도 자주 사용됩니다. 예를 들어 서버 프로세스의 로그 파일의 경우, 서버에서 요청을 받을 때마다 로그 파일을 업데이트합니다. 이 때 tail -f <FILE>과 같이 실행해두면, tail 프로그램이 종료되지 않고 업데이트되는 내용을 바로 출력해줍니다. 뒤에서 다루겠지만 tail 프로세스를 종료하고 셸로 돌아가고 싶을 때는 컨트롤 + C를 입력해주면 됩니다.

9. 헬프 옵션(-h, –help)으로 사용법 출력하기

명령어들의 사용법이 궁금하거나, 어떤 기능이나 옵션이 있는지 궁금하다면 도움말을 출력해보는 것을 추천합니다. 커맨드라인 명령어들은 일반적으로 도움말을 가지고 있습니다. 단, 명령어마다 도움말을 출력하는 방법은 다릅니다. 도움말을 출력하는 방법은 크게 3가지가 있습니다.

  1. 아무런 인자없이 명령어를 실행한다.
  2. -h 옵션을 붙여서 실행한다.
  3. --help 옵션을 붙여서 실행한다(가끔은 -help인 경우도 있습니다).

정답은 없습니다. REPL의 장점은 인터렉티브하게 여러 번 시도해볼 수 있는 점이니 부담없이 시도해보고 조금씩 적응해나가야합니다. 예를 들어 cd는 인자없이 실행하면 홈디렉터리로 이동해버립니다. 😅 grep 같은 경우는 인자없이 실행하면 기본적인 사용법으로 보여줍니다.

$ grep
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
        [-e pattern] [-f file] [--binary-files=value] [--color=when]
        [--context[=num]] [--directories=action] [--label] [--line-buffered]
        [--null] [pattern] [file ...]

이 사용법에도 사실 많은 관습들이 숨여있습니다. [] 안의 내용들은 생략이 가능하다는 의미입니다. 자세히 설명하지는 않았지만 ---로 시작하는 문자열은 옵션을 의미합니다. 즉, grep은 인자로 ---로 다수의 옵션을 지정할 수 있습니다. 실제 인자는 맨 마지막에 표시되어있습니다. 첫 번째 인자는 pattern이고 두 번째 인자는 file입니다. 여기서 file ...이라는 의미는 여러 개의 파일 경로를 지정할 수 있다는 의미입니다. 와, 이거 처음보면, 너무 어렵죠? 설명하면서도 느끼는 거지만 커맨드라인 세계는 익숙해지기 전까지는 너무나도 많은 커맨드라인 세계만의 상식과 관습에 지배되어있습니다.

아, 근데 grep이 뭐냐고요? 안타깝게도 여기에는 설명이 없네요. 이럴 때는 도움말이 아니라 매뉴얼을 봐야하는데, 그건 조금 뒤에서 다시 다루도록 하겠습니다.

grep은 그나마 양반입니다. -h--help 옵션 둘 다 지원하고 있습니다. 내용은 그냥 실행하는 것과 같아서 별 도움이 안 되지만요. 😆

$ grep -h
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
        [-e pattern] [-f file] [--binary-files=value] [--color=when]
        [--context[=num]] [--directories=action] [--label] [--line-buffered]
        [--null] [pattern] [file ...]

$ grep --help
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
        [-e pattern] [-f file] [--binary-files=value] [--color=when]
        [--context[=num]] [--directories=action] [--label] [--line-buffered]
        [--null] [pattern] [file ...]

ls-h 옵션의 의미가 용량 표시법을 바꾸는 옵션입니다. tail에도 한 번 -h 옵션을 붙여볼까요?

$ tail -h
tail: illegal option -- h
usage: tail [-F | -f | -r] [-q] [-b # | -c # | -n #] [file ...]

뭔가 usage라고 사용할 수 있는 옵션들을 보여주기는 합니다만, 그 위의 줄을 보면 h는 잘못된 옵션이라고 알려주고 있습니다. 이 말은 -h 옵션은 존재하지 않으니까, 사용법을 확인해봐라는 맥락에서 사용법을 알려주는 것입니다. 너무 실망하지는 마시기 바랍니다. 매우 친절하고 간결하게 사용법을 보여주는 커맨드라인 명령어들도 많이 있으니까요.

단, 모든 프로그램들은 케이스 바이 케이스이기 때문에 매번 직접 실행해보는 수밖에는 없습니다. 😭

10. man으로 명령어의 매뉴얼 읽기

명령어의 도움말은 사실 명령어가 어떤 용도인지 알고 있을 때 옵션을 보는 용도로 주로 사용합니다. 명령어 자체에 대한 정보는 도움말보다는 man 명령어로 매뉴얼을 열어서 확인할 수 있습니다.

$ man grep
GREP(1)                   BSD General Commands Manual                  GREP(1)

NAME
     grep, egrep, fgrep, zgrep, zegrep, zfgrep -- file pattern searcher

SYNOPSIS
     grep [-abcdDEFGHhIiJLlmnOopqRSsUVvwxZ] [-A num] [-B num] [-C[num]] [-e pattern] [-f file]
          [--binary-files=value] [--color[=when]] [--colour[=when]] [--context[=num]] [--label] [--line-buffered]
          [--null] [pattern] [file ...]

DESCRIPTION
     The grep utility searches any given input files, selecting lines that match one or more patterns.  By
     default, a pattern matches an input line if the regular expression (RE) in the pattern matches the input
     line without its trailing newline.  An empty expression matches every line.  Each input line that matches at
     least one of the patterns is written to the standard output.
...

아까보다는 훨신 더 친절하고 긴 문서를 확인할 수 있습니다. 이 문서를 통해서 프로그램에 대한 기본적인 설명과 옵션에 대한 해설 그리고 사용 예제까지고 확인해볼 수 있습니다. 설명을 읽어보면 grep은 텍스트 파일에서 패턴 매칭으로 검색을 하는 유틸리티라는 것을 알 수 있습니다.

엇, 그런데 현재 화면은 셸 프롬프트가 아니네요? 뭔가 달라졌네요…

man으로 명령어의 매뉴얼을 볼 수 있습니다

기본적으로 man은 페이저 프로그램 중 하나로 문서를 보여줍니다. 페이저는 커맨드라인 상에서 긴 문서를 좀 더 쉽게 볼 수 있도록 도와주는 도구입니다. 화살표를 이용해 위아래로 이동할 수 있고, Page Up(맥OSmacOS의 경우 fn + 위), Page Down(fn + 아래) 키로 페이지를 이동할 수도 있습니다. 맨 마지막 줄에는 : 다음에 명령어를 입력하는 커서가 위치합니다. 처음 들어오면 나가는 방법을 몰라 당황할 수 있습니다만, q를 누르면 종료할 수 있습니다.

다시 man으로 돌아와 Page Down으로 쭉 문서를 내려 Example 절을 찾아봅니다.

grep 매뉴얼의 예제 파트. 명령어의 간단한 사용법을 확인할 수 있습니다

grep 'patricia' myfile에서 첫번째 인자는 패턴, 두번째 인자는 검색하고자 하는 파일이 됩니다. 즉, 이 명령어는 myfile의 내용 중에 ’patricia’라는 내용이 있는지 검색하라는 의미입니다.

예를 들어 /etc/bashrc 파일에서 shell이라는 문자열이 있는 줄을 찾으려면 grep shell /etc/bashrc라고 입력합니다.

grep으로 파일에서 문자열을 찾는 예제

shell이 포함된 줄만 잘 출력해주는 것을 확인할 수 있습니다!

근데 도움말은 별로 도움이 안 되는 경우가 많고, man은 너무 공들여 쓰여진 고전적인 매뉴얼이죠? 😅 보통 man의 Example 부분이나 인터넷에서 검색해보는 게 명령어의 기본적인 사용법을 익히는 데 더 빠른 도움이 되긴합니다.

11. 왜 어떤 옵션은 -를 사용하고 어떤 옵션은 --를 사용하나요?

도움말 옵션을 자세히 보셨다면, 눈치 빠르신 분들은 -h--help의 차이를 발견하셨을 것입니다. h 문자를 옵션으로 사용할 때는 -을 하나만 쓰고 help 문자열을 옵션으로 사용할 땐 -를 두 번 사용합니다.

다음과 같은 옵션 규칙을 기억해두시면 편리합니다.

대부분의 커맨드라인 명령어는 이러한 관습을 따릅니다만, 반드시 그런 것은 또 아닙니다. 그래서 가끔은 도움말 옵션이 -help인 프로그램도 있으니 주의가 필요합니다.

-을 사용하면 여러개의 옵션을 한 번에 사용할 수 있습니다. 예를 들어서 앞에서 ls를 배울 때 ls -l -S -G -r 명령어를 사용해보았는데, 이 명령어는 ls -lSGr로 줄여서 사용할 수 있습니다. 아주 아주 많이 사용되는 형태입니다.

가끔씩 보이는 특이한 경우로는 <COMMAND> <OPTIONS> -- <ARGUMENT> 형식으로 --가 사용될 때도 있습니다. 이는 -- 앞에서 옵션 지정을 명시적으로 종료하고, 명시적으로 인자를 받는 경우에 활용됩니다. 드물게 사용하기는 하지만 당황하지 마시고, ‘그냥 이런 식으로도 쓰는구나’ 하면 됩니다.

일반적인 옵션 규칙에 대한 좀 더 자세한 내용은 다음 문서를 참고해주세요. 단, 아래 규칙은 어디까지나 일반적인 규칙일 뿐 100% 적용되는 것은 아니니 주의가 필요합니다.

12. 서바이벌 가이드: 비상 탈출 버튼 ^c, ^d, ^\, ^z

Space-cadet 키보드 이미지(소스: 위키피디아 Space-cadet keyboard 항목)

혹시 이런 키보드를 보신 적 있으신가요? 현재는 특수 입력을 위한 키로 Control, Option, Command, Alt, Shift 정도가 사용됩니다만(적고 보니 이것도 많네요 😅), 예전에는 Hyper, Super, Meta 같은 초월적인 특수 키들도 존재했습니다. 이런 키들은 독립적으로는 동작하지 않고, 다른 키와 조합해서 입력되는 수정키modifier라고 불립니다.*

* Emacs에서는 메타키가 아직도 사용됩니다. 일반적으로 알트 키가 대신 사용되긴 합니다만, 이름은 메타 키라고 부르고 알트 + x 키 조합을 M-x와 같이 표기합니다.

GUI 애플리케이션에서도 단축키를 입력할 때는 수정키를 주로 사용합니다만, 셸에서는 특히나 수정키와 커맨드라인 상의 관습적인 표현에 대해서 알아두는 게 중요합니다. 예를 들어 ^(캐럿 문자)는 무엇을 의미할까요? Shift + 6? 땡. 아닙니다. 캐럿문자는 컨트롤 키와 다른 키 조합을 의미 합니다. 예를 들어 ^c는 컨트롤 키를 누른 상태에서 c를 입력하는 걸 의미합니다. ^c 조합은 하나의 문자로 입력됩니다. 아스키 코드에서 십진수 97이 a 문자를 의미하는 것처럼, ^c는 아스키코드 십진수 기준 3에 맵핑되어있는 하나의 문자입니다. ^c가 2개의 문자가 아니라 하나의 문자라는 걸 이해하는 게 아주 중요합니다.*

* 맥OS의 GUI 애플리케이션에서는 수정키로 주로 커맨드 키를 사용합니다. 이는 윈도에서 컨트롤 키와 비슷한 역할을 합니다만, 맥에서는 커맨드 키와 컨트롤 키가 분리되어있다는 점을 주의할 필요가 있습니다. 터미널에서는 주로 컨트롤 키를 사용합니다. 맥OS의 GUI에서도 컨트롤 키는 ^ 문자로 표현합니다.

^c는 특히, 반드시, 무조건 기억해두어야합니다. 어떤 커맨드라인 프로세스가 실행되는 동안 이러한 특수한 문자를 사용해 프로세스에 어떤 신호(시그널)를 보내는 게 가능합니다. ^c를 입력하면 SIGINT(siginterrupt)라는 신호가 실행중인 프로세스에 전달되고, 일반적으로 현재 실행중인 작업을 중단합니다. 여기서 “일반적이라고” 이야기한 것은 이 시그널을 받았을 때 어떻게 처리할지는 프로그램 구현에 따라 다르기 때문입니다. 어떤 프로그램은 아무런 처리를 하지 않기도 하고, 어떤 프로그램은 완전히 중지되며, 또 어떤 프로그램은 사용자의 입력을 기다리는 상태가 됩니다.

어쨌건 지금 동작중인 작업을 중지 시킬 때 가장 먼저 해볼 수 있는 방법이 바로 ^c를 입력하는 것입니다. 이와 비슷한 문자(키)로는 SIGQUIT 시그널을 보내는 ^\와 EOF(End of File)을 의미하는 ^d, 어지간한 상황에서는 현재 실행중인 프로세스를 이 키들을 사용해서 빠져나가는 것이 가능합니다. 이외에 프로세스를 일시 중지시키는 SIGSTOP 신호를 보내는 ^z 키도 있습니다. ^z에 대해서는 좀 더 뒤에서 설명하도록 하겠습니다.

위의 어떤 키를 입력해도 먹통일 때는…, 그럴 때도 있습니다. 너무 고민하지 마시고 터미널을 종료하고 다시 실행해보시기 바랍니다.

그럼 cat을 사용해 ^c, ^d, ^\의 동작을 각각 살펴보겠습니다. cat은 첫 번째 인자로 출력하고자 하는 파일명을 받을 수 있습니다만, 인자 없이 실행하면 입력을 받고, 그 내용을 그대로 출력할 수도 있습니다. (이 기능은 어디에 쓰는 걸까요???)

$ cat
Hello, world
Hello, world

cat을 인자 없이 실행하고, Hello, world를 입력하고 엔터를 누릅니다. 그러면 그대로 Hello, world가 다음 줄에 출력되고 다시 입력 상태가 됩니다. 이제 그만 빠져나가고 싶습니다! ^c를 눌러봅니다.

$ cat
Hello, world
Hello, world
^C
$

^C라고 찍히고, 다시 셸 프롬프트로 돌아옵니다.

이번에는 cat을 입력하고, ^d를 입력해봅니다.

$ cat
^D
$

이번에는 ^D라고 찍히고, 다시 셸 프롬프트로 돌아옵니다. ^D는 조금 특별한 키입니다. 이 키는 End of File을 의미하는 특수한 문자로, 입력이 완전히 종료되었음을 알려줍니다.

마지막으로 cat을 인자 없이 실행하고 ^\를 입력해봅니다.

$ cat
^\Quit: 3
$

조금 출력이 다르죠? ^\가 출력된 다음 Quit: 3가 출력되고 셸 프롬프트로 돌아옵니다. ^c, ^d, ^\ 문자 입력 모두 cat을 종료하는 목적을 달성하기는 했지만 의미나 동작이 묘하게 다른 것을 확인할 수 있었습니다. 이는 앞에서 이야기했듯이 프로그램마다 시그널이나 EOF 입력을 처리하는 방식이 다르기 때문입니다.

13. 포그라운드(foreground) / 백그라운드(background) 작업

앞에서 프로그램을 중지하는 몇가지 단축키들에 대해서 소개했습니다만 ^z에 대해서는 제대로 설명하지 않았습니다. 이는 ^z를 이해하려면 포그라운드와 백그라운드 개념을 이해해야하기 때문입니다. 포그라운드(foreground) 작업은 현재 터미널에서 입력을 받고 있는 프로세스를 의미합니다. 터미널 앱을 실행하면 기본적으로 포그라운드에서 동작하는 앱은 바로 셸입니다. 앞에서 간단히 살펴보았지만 cat을 인자 없이 실행하면, 사용자의 입력을 cat 프로세스가 처리합니다. 따라서 셸에는 사용자의 입력이 전달되지 않습니다. 이 때는 바로 cat이 포그라운드에 있는 명령어라고 할 수 있습니다. ^ccat 프로세스를 종료시키고 나서야 비로소 셸에 입력을 전달할 수 있습니다.

이번에는 sleep이라는 명령어를 한 번 사용해보겠습니다.

$ sleep
usage: sleep seconds

sleep 명령어를 인자없이 실행하니 초를 인자로 같이 입력해서 사용하라고 합니다. 이름에서 유추할 수 있듯이 이 명령은 n초 동안 아무것도 하지 않고 대기합니다. sleep 5를 실행해서 동작을 확인해봅니다.

$ sleep 5

5초가 지나면 다시 셸 프롬프트로 돌아옵니다. 이번엔 99999초 동안 sleep을 해봅니다.

$ sleep 99999

걱정할 필요는 없습니다. 우리는 이미 앞서서 프로세스를 강제로 종료하는 ^c^\를 배웠습니다. 언제든지 이 키를 입력하면 프로세스가 종료됩니다. 단, sleep의 경우 텍스트 입력을 받는 프로그램은 아니므로 ^d는 처리하지 않습니다. 이번에는 조금 다른 접근을 해보겠습니다. 바로 ^z입니다.

$ sleep 99999
^Z
[1]+  Stopped                 sleep 99999
$ 

^z를 입력하면 위와 같이 sleep 99999 프로세스가 일시중지되었다는 메시지가 출력됩니다. 이는 프로세스가 종료되는 것과는 다릅니다. 중요한 점은 포그라운드에서 실행되던 sleep이 일시중지되고, 셸이 다시 포그라운드에서 실행되고 있다는 점입니다. 즉, 셸 위에서 다른 명령어를 입력할 수 있습니다.

하나 더 새로운 명령어를 배워보겠습니다. 바로 jobs입니다. 이 명령어는 현재 셸에서 일시중지되어있는 프로세스의 목록을 보여줍니다.

$ jobs
[1]+  Stopped                 sleep 99999

처음에 ^z를 입력했을 때와 같은 포맷으로 sleep 99999가 일시중지되어있다는 것을 보여줍니다.

프로세스가 포그라운드에 있지 않은 경우 사용자 입력을 받는 것이 불가능합니다. 사실 시스템 상에는 많은 프로세스가 실행중입니다만, 여기서 포그라운드 잡인지 백그라운드 잡인지는 현재 실행중인 셸의 맥락에서만 생각한다고 기억해주세요.

현재 상황에서 포그라운드 잡은 무엇일까요? 바로 셸입니다. 그리고 백그라운드에는 sleep 99999 프로세스 하나가 있습니다. 한 번 더 해보겠습니다.

$ sleep 99998
^Z
[2]+  Stopped                 sleep 99998

$ jobs
[1]-  Stopped                 sleep 99999
[2]+  Stopped                 sleep 99998

1번 잡과 2번 잡은 모드 중지(Stopped) 상태입니다. 이를 백그라운드나 포그라운드에서 실행을 재개할 수 있습니다. 백그라운드에서 프로세스가 재개된다는 의미는 사용자의 입력을 받을 수는 없지만, 그냥 자기가 할 일을 알아서 계속 수행하거나, 수행이 끝난 후에 종료된다는 것을 의미합니다. 백그라운드에서 실행을 재개하려면 bg 명령어를 사용합니다. bg는 기본적으로 마지막에 중지한 프로세스를 백그라운드에서 실행하지만, jobs의 번호(앞의 예에서 1 혹은 2)를 인자로 넘겨줄 수도 있습니다.

$ bg
[2]+ sleep 99998 &
$ jobs
[1]+  Stopped                 sleep 99999
[2]-  Running                 sleep 99998 &

이제 마지막에 중지했던 sleep 99998이 Running 상태가 된 것을 확인할 수 있습니다. 마지막에 보이는 & 문자는 백그라운드에서 동작한다는 걸 의미합니다.

이번에는 fg 명령어를 사용해 가장 마지막에 중지시켰던 잡을 포그라운드로 가져와보겠습니다.

$ fg
sleep 99998
^C

fg를 실행하면 sleep 99998이 포그라운드 잡이 되고, 사용자 입력을 받습니다. sleep은 따로 사용자 입력을 처리하지 않기 때문에 사용자는 꼼짝없이 99998초 동안 기다려야만 프로세스가 종료됩니다. 여기서는 ^c로 중지시키고, 나머지 sleep 99999fg로 포그라운드로 가져와서 중지시켜줍니다.

$ fg
sleep 99999
^c

다시 fg를 실행해봅니다.

$ fg
bash: fg: current: no such job

이제 백그라운드 잡이 남아있지 않아서 에러가 발생합니다. 정리를 해보면 ^z로 프로세스를 중지(stop)시킬 수 있고, jobs로 중지된 작업 목록을 확인할 수 있습니다.fg로 포그라운드로 가져오거나 bg로 백그라운드에서 작업을 재개할 수 있습니다.

가끔 ^c, ^d, ^\가 모두 먹통인 경우에도 ^z 키로는 프로세스를 중지시키는 게 가능한 경우가 있어서 편리하게 사용할 수 있습니다.

노트
셸에서 실행된 프로세스

앞에서 어떤 작업이 포그라운드 잡인지 백그라운드 잡인지는 현재 실행중인 셸에서 의미가 있다고 이야기하였습니다. 이 말을 이해하기 위해서는 셸에서 어떤 원리로 명령어가 실행되는지를 이해해야합니다.

예를 들어 설명해보겠습니다. 터미널 애플리케이션을 하나 띄우고 여기서 실행된 셸(셸1)에서 sleep 99999를 실행한 후 ^z로 중지시킵니다. 이 프로세스는 jobs 명령어로 확인할 수 있습니다. 그런데 터미널 애플리케이션을 하나 더 띄우고 여기서 실행된 셸(셸2)에서 jobs를 실행해보면 셸1에서 출력된 결과와는 달리 아무것도 출력되지 않습니다. 즉, 프로그램을 실제로 실행했던 셸에서만 중지시킨 잡을 확인할 수 있습니다.

이 글에서는 자세히 다루지 않습니다만, 프로세스는 트리 구조로 부모 자식 관계를 가집니다. 따라서 셸1에서 실행한 모든 명령어는 기본적으로 셸1 프로세스의 자식 프로세스가 됩니다. ^z 키나 jobs, bg, fg 명령어는 모두 특정 셸의 맥락에서만 실행됩니다. 즉, 셸1의 자식 프로세스만을 대상으로 합니다.

ps -a -o pid,ppid,command | grep $$* 명령어로 현재 셸과 현재 셸의 자식 프로세스들을 모두 확인해볼 수 있습니다. 셸1에서 이 명령어를 실행하면 다음과 같이 출력됩니다.

* 아직은 이 명령어가 어렵게 느껴질 수 있습니다만, ps| 표현에 대해서는 뒤에서 다룹니다.

$ ps -a -o pid,ppid,command | grep $$
37949  6969 bash
58799 37949 sleep 99999
60274 37949 ps -axwwo pid,ppid,command
60275 37949 grep 37949

첫 번째 컬럼은 프로세스의 ID, 두 번째 컬럼은 부모 프로세스의 ID(셸1), 세 번째 컬럼은 프로세스의 명령어를 보여줍니다. 첫 번째 줄은 셸1 자체의 정보입니다. 2번째 줄부터는 부모 프로세스의 ID가 모두 37949로 동일합니다. 여기에는 총 3개의 명령어가 실행중인데, 2번째 줄은 ^z로 실행 중지시킨 sleep 99999 프로세스입니다. 프로세스 탐색을 위해 3번째와 4번째 줄의 내용은 현재 실행중인 프로세스의 정보입니다.

14. 리눅스 시그널에 대해서 조금 더 알아보기: kill

시그널은 프로세스의 보내는 일종의 이벤트라고 할 수 있습니다. 앞에서 이야기했듯이 시그널에 대한 처리의 책임은 일반적으로 프로세스에 있기 때문에 항상 같은 방식으로 처리가 이루어지지 않습니다. 이미 우리는 앞서 SIGINT(^c), SIGQUIT(^\), SIGSTOP(^z) 3가지 시그널에 대해서 배웠습니다.

이외에 시그널들은 어떤 게 있을까요? 현재 터미널 환경에서 입력할 수 있는 특수문자들에 대해서는 stty -e로 확인할 수 있습니다. 이 중에는 앞서 확인한 ^\^z와 같이 시그널을 보내는 문자도 보입니다. 다른 키가 맵핑된 경우에도 이 명령어로 확인하는 것이 가능합니다.

stty로 현재 터미널에서 입력할 수 있는 특수문자를 확인

지금까지는 단축키를 사용해 포그라운드의 프로세스에 시그널 이벤트를 보냈지만, 명령어를 사용해 실행중인 특정 프로세스에 시그널을 보내는 것도 가능합니다. 이 때 사용하는 명령어가 바로 kill 입니다. 이름에서 유추 가능하듯이 일반적으로는 프로세스를 종료하기 위해 SIGTERM 시그널을 보내는 용도로 사용합니다. 하지만 원하는 시그널 이름이나 번호를 지정해서 보내는 것도 가능합니다. 사용할 수 있는 모든 시그널은 -l 옵션으로 확인할 수 있습니다.

$ kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2

이미 살펴본 바 있는 INT, QUIT, STOP도 확인할 수 있습니다. 이런 시그널들은 포그라운드 프로세스에서 주로 사용합니다. 이와 달리 앞에서 설명한대로 kill 명령어가 기본적으로 보내는 SIGTERM 시그널은 프로세스에 정상적인 종료 과정을 거쳐 종료하라는 이벤트입니다. SIGKILL이라고도 불리는 9번 시그널은 강제 종료를 하라는 명령어입니다.

시그널에는 각각 고유한 번호가 있어서 이름 대신 번호를 사용할 수 도 있습니다. 예를 들어 INT는 2, QUIT 3, TERM은 15번입니다. kill <PID>kill -9 <PID> 형식으로 매우 자주 활용하는 명령어이니 꼭 기억해두시기 바랍니다. 아직 프로세스 ID를 찾는 방법에 대해서는 소개하지 않았습니다만, sleepjobs를 사용해 kill로 종료하는 간단한 예제를 소개합니다.

$ sleep 10000
^z
[1]+  Stopped                 sleep 10000
$ bg
$ jobs -l
[1]+ 62506 Running                 sleep 10000 &

sleep 10000 명령어를 중지 시킨 후 백그라운드에서 실행시켰습니다. jobs-l 옵션을 붙이면 이 프로세스의 프로세스ID를 확인할 수 있습니다. 여기서 62506이 바로 sleep 10000 명령어의 프로세스 ID입니다.

앞에서는 fg로 이 프로세스를 포그라운드로 가져온 다음 ^c로 종료시켰습니다만, kill로 이 프로세스를 종료시켜보겠습니다.

$ kill 62506
bash-3.2$ jobs
[1]+  Terminated: 15          sleep 10000
bash-3.2$ jobs

kill 실행 직후 jobs에서 Terminated 상태로 변환된 것을 볼 수 있습니다만 잠시 후 jobs를 실행하면 더 이상 목록에 나오지 않는 것을 확인할 수 있습니다. kill이 보내는 SIGTERM은 정상 종료 명령어이므로 종료 과정에서 문제가 생기면 프로세스가 계속 남이있게 됩니다. 이럴 때는 kill -9 <PID>로 강제 종료하는 것이 가능합니다. 단, 데이터가 유실 되어서는 안 되는 중요한 작업이 진행중이라면 강제 종료는 피하는 것이 좋습니다.

셸 사용자 입장에서는 이 정도만 알아도 충분합니다. 시그널에 대해서는 더 자세한 내용은 일반적으로 시스템 프로그래밍이나 커널에서 주로 다루곤 합니다.

15. 라인 앞 뒤로 이동하기 ^a, ^e, 단어 단위로 이동하기 alt + 화살표

이제 셸 프롬프트에서 명령어를 입력하고 실행한다는 개념에는 어느 정도 익숙해지셨을 겁니다. 명령어를 입력할 때는 왼쪽, 오른쪽 화살표를 문자를 이동해 수정이 가능합니다. 명령어가 긴 경우 한 번에 맨 앞이나 맨 뒤로 이동하고 싶을 수 있습니다. Home이나 End 키가 있는 키보드라면 아마 똑같이 동작할 것입니다. Home이나 End 키가 없다면, 같은 용도로 사용할 수 있는 단축키가 바로 ^a와 ^e입니다. 외워두시면 편리합니다.

단어 단위로 이동할 때는 alt(option) + 왼쪽 또는 alt(option) + 오른쪽을 입력합니다.*

* 단어 이동은 alt + b와 alt + f로도 동작하지만 환경에 따라서 제대로 동작하지 않을 수 있습니다. ^a, ^e, alt+b, alt+f는 이맥스Emacs의 기본 조작에서 온 단축키들입니다.

셸이 익숙하지 않을 때는 기본적인 명령어 입력조차도 매우 어렵습니다. 정말 셸을 처음 사용하는 분들은 긴 명령어를 입력하고, 오타 수정을 위해 전체 명령어를 다 지운 다음 다시 처음부터 입력하는 경우도 비일비재합니다. 몇가지 이동 방법만 익혀둔다면 매우 편리하게 활용할 수 있으니 이동을 위한 키는 반드시 익혀두시는 것을 추천합니다.

노트
iTerm2의 키보드 설정

아직 소개하지는 않았습니다만, 맥OS에서는 기본 터미널 이외에 iTerm2라는 가상 터미널 애플리케이션을 많이 사용합니다. iTerm2의 기본 설정에서 option + 왼쪽 혹은 option + 오른쪽 키를 입력하면 의도한 대로 동작하지 않고 [D와 [C가 입력될 것입니다.

iTemr2의 키 설정 화면

iTerm2의 설정창(Command + ,)를 열고 Profiles 탭의 Keys 메뉴로 이동합니다. 아래쪽의 Presets를 클릭해서 Natural Text Editing을 선택합니다. 이제 의도한대로 동작할 것입니다.

16. 내가 입력했던 명령어 찾기: history와 ^r

왼쪽 화살표와 오른쪽 화살표가 명령어 입력 커서를 이동하는 기능을 한다면 위아래 화살표는 입력했던 명령어를 탐색하는 역할을 합니다. 빈 프롬프트에서 위 방향 화살표를 누르면 바로 직전에 입력했던 명령어가 나타납니다. 2번 누르면 그 전에 입력했던 명령어가 나타내고, 반복됩니다. 아래로 누르면 다시 반대로 돌아갑니다. 이건 모르는 분들에게 알려드리면 너무 좋아하는 팁입니다.

더 좋은 방법들이 있습니다. 바로 history 명령어 입니다. history <N>과 같이 사용하면 최근에 입력했던 N개의 명령어들을 보여줍니다.

$ history 10
  101  pwd
  102  nano
  103  nano
  104  ll
  105  l
  106  cat
  107  a
  108  history
  109  history --help
  110  history 10

여기서 복사해서 명령어를 입력하면 참 편리하겠죠? 긴 명령어들을 입력할 때는 특히나 많은 도움이 됩니다. 그런데 history에 있는 명령어를 수정없이 바로 실행하는 더 좋은 방법이 있습니다. 바로 !<NUMBER>입니다. 여기서 <NUMBER>history 명령어가 출력하는 첫 번째 컬럼의 번호입니다. 예를 들어 101번 히스토리 pwd를 실행하려면 !101을 실행하면 됩니다.

$ !101
pwd
/Users/44bits

이 예제에서는 !101pwd보다 길다는 게 함정입니다만 😂, 잘만 활용하면 명령어 입력하는 게 매우 편해집니다.

또 커맨드라인 입문자 분들이 많이들 모르시는 꿀팁이 있습니다. 바로 ^r입니다. 반복해서 이야기합니다만 ^는 컨트롤 키를 의미합니다. ^r을 입력하면 이전에 입력했던 명령어들을 검색할 수 있습니다. ^r을 입력하고, c를 입력하면 이전에 사용했던 명령어 중에 c가 들어간 명령어를 찾아줍니다. 다시 ^r를 입력하면 더 이전에 실행한 명령어를 계속 찾습니다.

Ctrl + r로 이전에 입력했던 명령어를 검색하는 화면

문자 뿐만 아니라 단어로도 매칭이 됩니다. 원하는 명령어를 찾고, 엔터를 누르면 바로 실행됩니다. 바로 실행하고 싶지 않으면 왼쪽, 오른쪽 화살표를 눌러 편집할 수 있습니다. ^c를 누르면 명령어 탐색이 취소됩니다.

히스토리 관련 기능 몇 가지만 알아도 셸에서 명령어 입력하는 게 훨씬 더 즐거워집니다.

17. 환경변수(Environment variable): 프로세스의 동작에 영향을 주는 시스템 변수들

앞에서 살짝 환경변수 맛보기만 보여드렸던 거, 기억 나시나요?

$ echo My home directory is $HOME.
My home directory is /Users/44bits.

환경변수는 셸이 실행중인 환경의 시스템과 관련된 정보를 담은 변수들을 의미합니다. 위에서 $HOMEHOME이라는 이름을 가진 환경변수이고, 이 변수에는 /Users/44bits 값이 들어있습니다. echo는 셸에서 실행되는 명령어이고, 셸 환경에서 정의되어있는 환경변수들은 셸에서 실행한 명령어에 그대로 전달됩니다. 따라서 프로세스에서 환경변수의 값을 참조하는 경우, 환경변수의 값에 따라서 동작이 달라질 수 있습니다.

환경변수의 값을 확인하는 가장 기본적인 방법은 위와 같이 echo를 사용하는 것입니다. 이 경우 시스템에 정의된 환경변수의 이름을 미리 정확히 알고 있어야합니다.

$ echo $TERM
xterm-256color
$ echo $HOME
/Users/44bits
$ echo $USER
44bits

echo대신 printenv를 명시적으로 환경변수를 확인하는 방법도 있습니다.

$ printenv HOME
/Users/44bits

현재 셸에 정의되어있는 환경변수 전체 목록을 보고 싶을 때는 env 명령을 인자 없이 실행해봅니다.

$ env
TERM=xterm-256color
SHELL=/bin/zsh
TMPDIR=/var/folders
...

수십가지의 환경변수 목록이 정의된 것을 확인할 수 있습니다. 대부분의 환경변수는 셸이 실행되는 시점이 동적으로 정의됩니다만, 환경변수는 셸 위에서 원하는대로 정의하거나 덮어쓰는 것이 가능합니다. 환경변수를 정의할 때는 export를 사용하고, 삭제할 때는 unset을 사용합니다.

$ echo My name is $MYNAME
My name is
$ export MYNAME=nacyot
$ echo My name is $MYNAME
My name is nacyot
$ unset MYNAME
My name is

또 하나 많이 사용하는 기법은 특정 프로그램을 실행할 때만 환경변수를 적용하는 방법입니다. 명령어 앞에 환경변수이름=값 형식으로 환경변수를 정의한 다음 한 칸 띄워줍니다.

$ MYNAME=nacyot printenv MYNAME
nacyot
$ printenv MYNAME

첫 번째 명령어를 실행할 때는 nacyot이라고 출력이되었습니다만, 이 명령어가 끝난 다음 다시 printenvMYNAME을 출력해보면 아무것도 나오지 않습니다. 환경변수로 프로그램의 동작을 제어할 때 자주 사용하는 기법입니다.

18. 변수 선언하고 사용하기

환경변수 이외에 현재 셸에서만 사용가능한 변수를 선언하고 사용하는 것도 가능합니다. 환경변수와 명확히 구분되는 것은 아닙니다만, 관습적으로 환경변수 이름은 대문자를 사용하고, 일반 변수 이름은 소문자를 사용합니다.

환경변수는 앞에서 소개한대로 export 명령어로 선언하는 반면, 변수는 export 없이 선언합니다.

$ variable=356

변수로 선언된 값은 env로 확인이 불가능하며, printenv로는 출력되지 않습니다. 대신 환경변수와 마찬가지로 $에 이름을 붙여서 출력하는 것이 가능합니다.

$ printenv variable
$ echo $variable
356

셸에서는 주로 환경변수를 활용하고, 변수는 주로 셸 스크립팅에서 주로 사용됩니다만 결국 셀이 셸 스크립팅 환경이기도 해서 알아두면 도움이 됩니다.

19. 문자열 이해하기: 쌍따옴표와 홑따옴표의 차이

셸에서 문자열을 다루는 것은 생각보다는 까다로운 편입니다. 예를 들어 다음 명령어를 생각해보겠습니다.

$ echo Hello, world.
Hello, world.

얼핏 봐서는 Hello, world!를 출력해주는 간단한 명령어입니다만, Hello, world는 하나의 인자일까요? 두 개의 인자일까요? 내부적으로 echo 이 내용을 어떻게 처리하는 걸까요? 이를 좀 더 명확하게 하기 위해서 따옴표로 감싸서 명시적으로 문자열을 사용할 수 있습니다. 쌍따옴표와 홑따옴표 모두 사용이 가능합니다. 먼저 쌍따옴표에 대해서 알아보겠습니다.

$ echo "Hello, world."
Hello, world.

정상적으로 실행됩니다. 중요한 점은 따옴표를 사용하지 않았을 때와 달리 Hello, world. 하나의 인자로 전달된다는 점입니다. 따옴표 없이 사용하는 경우 일반적으로 빈 칸(스페이스)이 인자를 구분하는 용도로 사용이 됩니다. 쌍따옴표는 중요한 특징을 가지고 있습니다. 배시 셸의 문법에 따라 특수한 입력값들이 자동적으로 치환(interpolation)된다는 점입니다. 예를 들어 환경변수를 사용할 수 있습니다.

$ echo "Hello, $USER."
Hello, 44bits.

만약 환경변수를 출력하고 싶지 않다면 다음과 같이 $를 이스케이프 처리해야합니다. 쌍따옴표안에서 쌍따옴표를 쓰고 싶다면, 역시 이스케이프를 해야합니다. 쌍따옴표를 잘 활용하기 위해서는 변환 규칙이나 이스케이프 문자에 대해서 미리 알고있어야합니다.

$ echo "\"Hello, \$USER.\""
"Hello, $USER."

이외에도 셸의 규칙에 따라서 다양한 치환이 이루어집니다. 더 자세한 정보는 Bash 매뉴얼의 Shell Expansions에서 확인해볼 수 있습니다.

노트
줄바꿈과 탭 문자 출력

echo를 그냥 사용하면 \n\t를 그냥 출력해버립니다. 줄바꿈 문자와 탭 문자를 출력하려면 -e 옵션을 사용하면 됩니다.

$ echo "Hello,\n\t$USER."
Hello,\n\t44bits.
$ echo -e "Hello,\n\t$USER."
Hello,
    44bits.

이와 달리 홑따옴표는 아무것도 치환하지 않고 입력한 그대로의 내용을 전달합니다.

$ echo 'Hello, world.'
Hello, world.
$ echo 'Hello, $USER.'
Hello, $USER.

환경변수가 변환되지 않고 그대로 출력되는 것을 알 수 있습니다. 말그대로 입력한 그대로의 내용을 전달할 때는 매우 유용합니다만, 문제가 하나 있습니다. 홑따옴표안에서는 홑따옴표를 사용할 수가 없습니다. 엄청나죠? 이스케이프해도 안됩니다.

$ echo '''
> ^c
$ echo '\''
> ^c

엔터를 눌렀을 때 >가 뜨는 것은 셸이 문자열 입력이 끝나지 않았다고 판단하기 때문입니다. 즉, 홑따옴표 문자열이 끝나지 않고 입력중이라고 판단한 것입니다. 결론부터 이야기하자면 홑따옴표 문자열에서 홑따옴표를 쓰는 방법은 없습니다. 😦 (온갖 셸 흑마법을 동원한다면 가능할지도 모르지만요…)

대신 홑따옴표와 쌍따옴표의 중간 정도 역할을 하는 ANSI-C 방식을 사용할 수 있습니다. ANSI-C 방식은 홑따옴표처럼 거의 그대로 출력하면서도 홑따옴표(')의 이스케이프를 지원합니다.

$ echo $'Hello, \'$USER\''
Hello, '$USER'

잘 동작하는 것을 확인할 수 있습니다. ANSI-C 방식의 이스케이프 규칙에 관해서는 Bash 매뉴얼의 ANSI-C Quoting 절을 참고해주세요. (실제로 ANSI-C 방식까지 사용하는 건 거의 못 봤습니다.)

노트
Hello, world!

쌍따옴표 안에서 !를 사용하면 의외로(?) 문제가 발생합니다.

bash-3.2$ echo -e "Hello, world!"
bash: !": event not found

앞에서 잠깐 다뤘습니다만, !는 셸에서 히스토리 관련 치환을 하는 특수한 문자로 사용됩니다. 이 문제도 해결하기가 쉽지 않습니다. 꼭 해결해야한다면 set +H 명령어로 히스토리 관련 치환 기능을 비활성화하는 방법이 있습니다.

bash-3.2$ set +H
bash-3.2$ echo -e "Hello, world!"
Hello, world!

20. 셸에서 자주 사용하는 치환 기법 ${}, $(), $(())

앞서 이야기한 것처럼 쌍따옴표 문자열에서는 셸의 규칙에 따라서 치환이 일어납니다. 여기서 유용하게 사용할 수 있는 치환 기법 몇 가지를 소개하고자 합니다. 치환은 쌍따옴표를 사용하지 않을 때도 일어납니다만, 명시적인 표현을 위해서 쌍따옴표 안에서 사용하는 것을 추천합니다.

먼저 자주 사용되지는 않지만, 비교적 간단한 기법부터 소개해보겠습니다. $(())는 괄호 사이에 계산식을 넣으면 계산된 결과로 치환됩니다.

$ echo "2**10 == $((2**10))"
2**10 == 1024

2의 10승이 계산되어 1024라는 값이 출력되었습니다. 셸에서 사용가능한 모든 연산자에 대해서는 Shell Arithmetic 문서를 참고해주시기 바랍니다.

두 번째 기법은 소괄호가 하나만 있는 $()기법입니다. 이 기법을 사용하면 소괄호 안에 있는 명령어를 먼저 실행하고, 그 내용으로 치환을 합니다. 예를 들어 $(pwd)라고 사용하면, 현재 디렉터리로 치환됩니다.

$ echo "Current directory is $(pwd)."
Current directory is /Users/44bits/work.

어떤 원리로 돌아가는지 감이 오시나요? 이 기법은 출력뿐만 아니라, 명령어의 인자값에 다른 명령어의 실행 결과를 넣을 때도 자주 사용하는 방법입니다.

마지막으로 소개할 기법은 ${}입니다. 이 기법을 사용하면 셸의 변수 혹은 환경변수로 치환하거나 변수의 값을 변환하는 기능을 지원합니다. 저는 주로 환경변수 사용 명시적으로 보여주고 싶을 때 이 기법을 사용합니다.

$ echo "My home is $HOME."
My home is /Users/44bits/work.
$ echo "My home is ${HOME}."
My home is /Users/44bits/work.

환경변수와 문자열을 결합할 때도 유용하게 사용됩니다.

$ echo "My name is $USER_nacyot."
My name is .
$ echo "My name is ${USER}_nacyot"
My name is 44bits_nacyot

왜 이런 결과가 출력되었을까요? 위의 예제에서는 환경변수 이름을 명확히 구분할 수 없기 때문에 $USER_nacyot이 환경변수 이름으로 해석되었습니다. 따라서 빈 값이 출력됩니다.

${} 치환 기법은 몇 가지 유용한 문법들을 지원하고 있습니다. 간단한 기능을 한 번 살펴보겠습니다.

$ printenv MYNAME
$ echo "My name is ${MYNAME:-nacyot}"
My name is nacyot

예를 들어 위의 예제에서 볼 수 있듯이 존재하지 않는 변수를 사용하려고 하는 경우 기본값을 지정할수 있습니다. 일단 이 정도만 알아두어도 큰 문제는 없습니다. 그 외의 ${} 치환 기법에서 사용하능한 모든 문법은 Shell Parameter Expansion 문서를 참고해주시기 바랍니다.

21. 표준입출력과 리다이렉트

하나의 프로세스는 3개의 표준 스트림과 연결됩니다. 하나는 입력을 위한 STDIN이고, 나머지 두 개는 출력을 위한 STDOUT과 STDERR입니다. 이해를 위해 cat을 실행하고 Hello, world!를 입력하고 엔터를 쳐봅니다. 그럼 다음 줄에 Hello, world!가 출력됩니다.

$ cat
Hello, world!
Hello, world!
^C

첫 줄에 입력한 내용이 STDIN을 통해서 cat에 전달된 내용이고, 두 번째 출력된 내용이 STDOUT을 통해서 출력된 내용입니다.

화면에 출력되는 내용은 STDOUT을 사용하는 게 일반적입니다만, 에러를 사용하는 스트림을 분리해서 STDERR에 출력하기도 합니다. STDOUT과 STDERR은 화면 상에서 기본적으로 구분되지 않습니다. 추가적으로 프로세스 관점에서는 파일 디스크립터로 STDIN(0), STDOUT(1), STDERR(2)에 접근하는 것이 가능합니다.

STDIN으로 파일의 내용을 읽어오거나, STDOUT이나 STDERR의 내용을 파일에 기록할 수 있는데 이를 리다이렉트(redirect)라고 부릅니다. 예를 들어 >를 사용하면 표준출력을 화면에 출력하는 대신 파일에 내용을 기록할 수 있습니다.

$ ls
$ echo 'Hello, world!'
Hello, world!
$ echo 'Hello, world!' > hello.txt
$ ls
hello.txt
$ cat hello.txt
Hello, world!

먼저 ls로 현재 디렉터리에는 파일이 없는 것을 확인합니다. echo'Hello, world!'를 출력하면 바로 다음줄에 표준출력으로 Hello, world!가 출력됩니다. 그 다음에는 >를 사용해서 출력 내용을 hello.txt로 리다이렉트합니다. hello.txt 파일이 생성되었고, 이 파일에는 Hello, world!가 기록되어있습니다. 단, 리다이렉트를 할 때는 화면에는 출력이 안 되고, 파일에만 기록이 된다는 점에 주의해주세요.

이 파일의 내용을 <으로 프로세스에 넘겨주는 것도 가능합니다. cat으로 간단한 예제를 살펴보겠습니다.

$ cat < hello.txt
Hello, world!

< hello.txtcat을 인자없이 실행하고 Hello, world!\n^D를 입력한 결과와 같습니다.

또 자주 사용되는 리다이렉트 방법으로 >>이 있습니다. >는 사용할 때마다 기존 파일의 내용을 덮어씁니다만, >>를 사용하면 기존 파일 내용을 그대로 놔두고 맨 아래에 표준 출력의 내용을 기록합니다.

$ echo 'Foo' > 1.txt
$ cat 1.txt
Foo
$ echo 'Bar' > 1.txt
$ cat 1.txt
Bar

$ echo 'Foo' >> 2.txt
$ cat 2.txt
Foo
$ echo 'Bar' >> 2.txt
$ cat 2.txt
Foo
Bar

>>>를 사용하면 모든 표준출력은 파일로 리다이렉트됩니다.

22. 표준에러와 리다이렉트

자, 그럼 흥미로운 예제를 한 번 보겠습니다.

$ cat not_exist.txt > message.txt
cat: not_exists.txt: No such file or directory
$ cat message.txt

음? 이상하네요. 리다이렉트를 했는데, 화면에 출력이 찍힙니다. 그리고 리다이렉트로 message.txt 파일은 만들어졌는데 아무런 내용도 찍혀있지 않습니다. 눈치가 빠르신 분들은 알아채셨겠지만, 바로 위에서 출력된 내용이 STDERR을 통해서 출력된 내용입니다. STDOUT과 STDERR은 서로 다른 스트림이기 때문에 기록이 되지 않습니다.

그럼 STDERR의 내용을 리다이렉트하려면 어떻게 해야할까요? >이나 >>앞에 2를 붙여주면 됩니다. 2>이나 2>>와 같이 사용할 수 있습니다. 실제로 해볼까요?

$ rm message.txt
$ cat not_exist.txt 2> message.txt
$ cat message.txt
cat: not_exists.txt: No such file or directory

이번에는 또 다른 경우입니다. cat은 여러개의 인자를 입력받아서 여러 개의 파일 내용을 출력할 수 있습니다. 하나는 존재하는 파일이고, 하나는 존재하지 않는 경우에는 어떻게 될까요?

$ echo 'FooBar' > exist.txt
$ cat exist.txt not_exist.txt
FooBar
cat: not_exist.txt: No such file or directory

그럼 퀴즈입니다. >2>로 각각 리다이렉트한 결과는 어떻게 될까요? 정답은 함께 살펴보겠습니다.

$ cat exist.txt not_exist.txt > 1.txt
cat: not_exist.txt: No such file or directory
$ cat 1.txt
FooBar

$ cat exist.txt not_exist.txt 2> 2.txt
FooBar
$ cat 2.txt
cat: not_exist.txt: No such file or directory

1.txt 파일에는 표준출력의 내용이 저장되고, 2.txt에는 표준에러의 내용이 저장되었습니다. 여기까지는 지금까지 배운 내용으로 이해가 됩니다만, 만약 STDOUT과 STDERR의 내용을 한꺼번에 저장하려면 어떻게 해야할까요? 이 때 사용할 수 있는 방법이 바로 2>&1입니다. >로 리다이렉트한 이후, 2>&1를 명령어 맨 뒤에 붙여주면 STDERR(STDERR의 파일 디스크립터 2)의 내용을 STDOUT(STDOUT의 파일 디스크립터 1)로 보내줍니다. 따라서 STDOUT과 STDERR의 내용이 모두 파일에 저장됩니다.

$ cat exist.txt not_exist.txt > 3.txt 2>&1
$ cat 3.txt
FooBar
cat: not_exist.txt: No such file or directory

리다이렉트를 활용한 > /dev/null 2>&1라는 표현도 관습적으로 많이 사용됩니다. > /dev/null에 어떤 내용을 리다이렉트하면 그 내용은 버려집니다. 2>&1을 붙여서 표준에러의 내용을 표준출력에 보내면, 모든 출력 내용이 버려집니다.

$ cat exist.txt not_exist.txt > /dev/null 2>&1

어떤 내용이 출력되었는지 전혀 알 수 없죠? 어디에 쓰나 싶을 수도 있습니다만 화면에 출력되는 게 너무 많아서 번거로울 때 사용하곤 합니다.

23. 화면에 출력하고 동시에 파일에 기록을 해주는 tee

리다이렉트는 간단한 쓰기 작업을 할 때 편리합니다만, 리다이렉트해서 파일에 기록하는 내용을 화면에는 출력해주지 않습니다. 이 내용을 확인하려면 굳이 한 번 더 cat을 사용해서 파일의 내용을 출력해야하는 번거로움이 있습니다. 이럴 때 활용해볼 수 있는 명령어가 바로 tee입니다. tee는 주로 |와 함께 사용됩니다.

$ echo 'Hi, world!' | tee sayhi.txt
Hi, world!
$ cat sayhi.txt
Hi, world!

명령어를 실행할 때 화면에도 출력하고, 파일에도 저장된 것을 확인할 수 있습니다. 하지만 큰 문제가 있습니다. 아직 이 글에서는 | 파이프에 대해서 다루지 않았습니다. 그럼 |를 사용하지 않고 tee를 사용해보겠습니다. tee를 단독으로 실행하면 cat처럼 사용할 수 있습니다. 첫 번째 인자로는 입력한 내용을 저장할 파일 이름을 넘겨줍니다.

$ tee sayhello.txt
Hello, world!
Hello, world!
^C
$ cat sayhello.txt
Hello, world!

tee를 실행하고 Hello, world!\n을 입력하면 cat처럼 입력한 내용을 그대로 다시 출력해줍니다. 그리고 프로세스를 종료한 후 sayhello.txt를 출력해보면 입력한 내용이 저장된 것을 확인할 수 있습니다.

24. 서바이벌 가이드: 실수로 실행한 vim, emacs 탈출하기

에디터 없이 여기까지 왔습니다만, 커맨드라인에서 텍스트 파일을 편집하는 에디터 하나 정도는 다룰 줄 알면 매우 편리합니다. 커맨드라인 텍스트 에디터로는 주로 빔Vi Improved과 이맥스Emacs가 이야기됩니다만, 둘 다 매우 독창적이고 그만큼 다루기 어려운 도구입니다. 사실 데스크탑 환경이라면 굳이 커맨드라인 에디터를 사용하지 않아도 비주얼 스튜디오 코드나 평소에 사용하던 GUI 에디터를 사용해도 무방합니다. 당장 커맨드라인 에디터가 꼭 필요하다면 nano를 추천합니다. Vim이나 Emacs와 달리 비교적 단순한 인터페이스를 가지고 있습니다.

여기서는 Vim이나 Emacs를 다루는 대신 실수로 실행한 Vim이나 Emacs를 종료하는 방법에 대해서 소개하고자 합니다. 간단하지만 처음 실행해보면 당황스럽습니다. 먼저 vim을 실행해봅니다.

Vim을 실행 화면

^C, ^D, ^\는 먹지 않습니다. 화면에 출력된 내용을 보면 Vim을 종료하는 방법이 쓰여져있습니다. Vim에는 크게 명령 모드와 편집 모드가 있는데, vim을 인자없이 실행하면 명령 모드로 시작됩니다. 이 상태에서 차례대로 :, q를 입력하고 엔터를 누르면 프로그램이 종료됩니다.

단, 특정 파일을 편집하는 상태로 실행되거나, 다른 키를 입력하는 과정에서 편집 모드가 된 경우에는 이 방법으로 종료가 되지 않습니다. 이 때는 ESC를 누르면 명령 모드가 되고, 이 상태에서 :q!를 차례로 입력하고, 엔터를 눌러서 탈출할 수 있습니다.

emacs도 실행하고, 첫 화면을 유심히 보면 탈출 방법이 나와있습니다.

Emacs 실행 화면

Exit Emacs 부분을 찾아보면 C-x C-c라고 나와있습니다. 조금 독특한 표현법입니다만 여기서 C-x는 컨트롤+X를 의미합니다. C-x C-c는 컨트롤+X를 입력한 다음 컨트롤+C를 차례대로 입력한다는 의미입니다. 이 때 컨트롤 키는 누르고 있어도 됩니다.

만약 파일을 편집중인 상태라면, C-x C-c를 입력하면 맨 아래줄에 파일을 저장할 건지 물어봅니다. 여기서는 탈출이 목적이므로 n 엔터, yes 엔터를 차례로 입력해봅니다.

실수로 실행한(!) Emacs를 종료해봅니다

정상적으로 종료될 것입니다.

이도 저도 모르겠다. 알 수 없는 상태가 되서 너무 어렵고, 도저히 안 될 것 같다 싶을 때는 ^z를 활용해보시기 바랍니다. 프로세스가 중지됩니다. 무사히 빠져나오고 나서 앞에서 배운대로 jobs -lkill을 사용해 프로세스를 종료시켜주면 됩니다.

^Z
[1]+  Stopped                 emacs -q hello.txt
$ jobs -l
[1]+ 17508 Suspended: 18           emacs -q hello.txt
$ kill -9 17508

25. 간편하고 편리한 커맨드라인 에디터 GNU nano

커맨드라인에서 Vim이나 Emacs를 화려하게 사용하는 고수들을 보면 내심 커맨드라인에 경외심이 들지도 모릅니다만, 실용주의적인 관점에서 본다면 학습 비용이 큰 Vim이나 Emacs를 굳이 배울 필요는 없습니다. nano는 커맨드라인의 메모장과 같은 에디터입니다. 사실 메모장보다는 조금 더 강력합니다만, 메모장 수준에서만 사용하고자 한다면 학습 비용이 전혀 들지 않습니다.

nano를 단독으로 실행해보겠습니다.

$ nano
nano를 실행한 화면. 메모장처럼 사용할 수 있습니다

아주 깔끔하네요. 이 빈화면을 버퍼라고 부릅니다. 파일로 저장되기 전에 텍스트를 작성하는 공간이라고 생각하시면 됩니다. 여기서 자유롭게 텍스트를 입력합니다. 화살표로 이동도 가능합니다.

nano에 텍스트를 입력한 화면. 아래 쪽에 단축키 목록이 있습니다

또한 사용할 수 있는 단축키도 화면 아래쪽에 잘 나와있습니다. 텍스트를 작성했으니, 이 내용을 그대로 저장해보겠습니다. 아래에 나와있는대로 ^X를 눌러 종료를 진행합니다.

nano: ^x를 입력하면 수정한 내용을 저장할 지 물어봅니다

이번에도 선택할 수 있는 옵션들이 아래에 출력됩니다. 현재 버퍼를 저장할 지 물어봅니다. 저장할 생각이니 Y를 입력합니다.

nano: 저장할 파일 이름을 물어봅니다

마지막으로 File Name to Write: 뒤에 저장할 파일 이름을 입력하고 엔터를 누르면, 파일이 저장되고 nano가 종료됩니다.

이미 존재하는 파일을 편집하거나, 미리 생성할 파일의 이름을 지정할 경우 nano의 첫 번째 인자로 경로를 넘겨주면 됩니다.

nano가 커맨드라인에서 가장 좋은 에디터는 아닙니다만, 커맨드라인에 익숙하지 않더라도 직관적으로 활용할 수 있는 에디터입니다. 서버 환경과 같이 커맨드라인 에디터밖에 사용할 수 없을 때, Vim이나 Emacs를 사용할 줄 모른다면 유용하게 사용해볼 수 있습니다. 하지만 데스크탑 환경에서는 비주얼 스튜디오 코드와 같은 GUI 텍스트 에디터를 굳이 사용하지 않을 이유가 없습니다.

노트
셸에서 VS Code 바로 실행하기

커맨드라인 환경에서 텍스트 파일을 편집하고 싶을 때 바로 GUI 에디터에서 열 수 있습니다. 여기서는 VS Code를 셸에서 바로 실행하는 설정 방법에 대해서 소개해보겠습니다. 먼저 Code를 실행하고, Command + Shift + P를 눌러 커맨드 팔레트를 실행하고, ‘Command: Install ’code’ command in PATH’ 명령을 검색해서 실행합니다.

비주얼 스튜디오 코드에서 커맨드라인 명령어 code를 등록

이제 셸에서 바로 code를 실행할 수 있습니다. code .으로 실행하면 현재 디렉터리의 컨텍스트에서 VS Code가 실행되고, 파일명을 지정하면 이미 존재하는 파일을 편집하거나 새로운 파일을 작성할 수도 있습니다.

$ code nacyot.txt

앞서 GNU nano에서 편집하던 nacyot.txt 파일을 VS Code에서 바로 편집할 수 있습니다.

커맨드라인에서 바로 VS Code를 실행시킨 화면

26. 멀티라인 명령어 실행하기

셸에서는 \ 문자로 여러줄에 걸쳐 명령어를 실행할 수 있습니다.

$ echo 'Hello, world!'
Hello, world!
$ echo \
    'Hello, world!'
Hello, world!

결과는 같습니다. 다음과 같이 아주 긴 명령어를 입력할 때는 매우 읽기가 어려운데, 멀티라인으로 옵션을 좀 더 명확하게 드러낼 수 있습니다.

$ docker run -d \
    -e MYSQL_ALLOW_EMPTY_PASSWORD=true \
    -e MYSQL_DATABASE=wp \
    -e MYSQL_USER=wp \
    -e MYSQL_PASSWORD=wp \
    --name mysql \
    --network=app-network \
    mysql:5.7

단, \을 사용할 때는 반드시 \ 문자 뒤로 아무런 내용이 없어야합니다. \ 뒤에 스페이스가 들어가면 이스케이프로 해석되어버리기 때문에 의도한대로 동작하지 않습니다. 긴 명령어를 다룰 때는 디버깅이 어렵고, 명령어 히스토리가 지저분해지는 문제가 있습니다.

멀티라인 명령어를 셸에서 바로 입력하는 대신 에디터를 사용하면 좀 더 편리합니다. 셸에서 ^x ^e를 연속해서 입력하면 $EDITOR 환경변수에 지정된 에디터가 바로 실행되고, 여기서 편집한 내용을 저장하면 셸에서 바로 실행해줍니다.

VS Code를 사용하고자 하는 경우 EDITOR 변수에 code -w를 지정해줍니다.

$ export EDITOR='code -w'
$ ^x ^e

에디터가 실행되면, 실행하고자 하는 명령어를 작성합니다. Command + S로 저장하고, Command + W로 에디터를 종료합니다.

비주얼 스튜디오 코드에서 긴 명령어를 편집해봅니다

종료하면 바로 셸에 편집한 내용이 복사되고 실행됩니다.

에디터를 종료하면 bash 셸에서 명령어가 바로 실행됩니다

이에 대한 더 자세한 내용은 아래 문서를 참고해주시기 바랍니다.

27. 탭으로 자동 완성

셸에서 활용해볼 수 있는 보편적인 인터페이스 중에 하나가 바로 탭입니다. 탭은 주로 현재 컨텍스트에서 사용할 수 있는 후보들을 보여주는 자동완성으로 사용됩니다. 예를 들어 셸에서 입력하는 첫번째 단어는 실행하려는 프로그램의 이름(혹은 경로)입니다. c를 입력하고, 탭을 누르면 c로 시작하는 프로그램 이름들을 보여줍니다.

명령어 자동완성: 자동 완성 후보가 많아서 정말로 보고 싶은지 물어봅니다

후보가 너무 많아서 정말 다 보여줄지 물어봅니다. y를 누르면 c로 시작하는 실행가능한 명령어들의 목록을 보여줍니다.

자동 완성 결과를 페이징할 수 있습니다. 스페이스 키로 넘어갑니다

후보가 많다보니 심지어 페이징까지 지원합니다. 스페이스를 누르면 다음 페이지로 넘어갑니다.

이번에는 명령어를 입력하고 첫 번째 인자 자리에서 탭을 눌러봅니다. 현재 디렉터리의 파일이 후보로 표시됩니다.

$ cat <TAB>
airport     nacyot.txt

이번에는 한 글자를 입력하고, 탭을 눌러보겠습니다.

$ cat n<TAB>

후보가 하나밖에 없기 때문에 아래와 같이 자동완성 되는 것을 확인할 수 있습니다.

$ cat nacyot.txt

여기서 보여드린 건 Bash에서 바로 사용가능한 단순한 수준의 자동 완성입니다만, 셸 설정 등에 따라서 더 다양하고 강력한 자동완성도 사용할 수도 있습니다. 단순하건, 고도화되어있건 셸에서는 자동완성으로 탭을 사용한다는 사실을 기억해주세요.

28. 숨김 파일: .으로 시작하는 파일과 디렉터리들

맥OS나 리눅스에도 숨김 파일이 있는 것을 알고계시나요? .으로 시작하는 파일들은 여러 애플리케이션에서 숨겨져있는 파일로 취급됩니다. 예를 들어 .으로 시작하는 파일은 맥OS의 파인더에서도 기본적으로 보여주지 않습니다. ⌘ + ⇧ + . 단축키를 입력하면, 그제서야 숨겨져있는 파일이 나타납니다.

.으로 시작하는 파일을 파인더에서 볼 수 있습니다.

커맨드라인 명령어 ls에서도 마찬가지입니다. ls를 사용할 때 습관적으로 -a 옵션을 붙이는 경우가 있는데, 이 옵션을 붙여야만 .으로 시작하는 파일을 포함한 모든 파일의 목록을 보여주기 때문입니다. 숨김 파일이라고 파일 작업까지 특별하게 처리되는 것은 아닙니다.

.으로 시작하는 파일은 주로 설정 파일을 저장하는 용도로 많이 사용됩니다. 특히 $HOME 디렉터리 아래에는 커맨드라인이나 GUI 애플리케이션의 사용자 설정을 담은 .으로 시작하는 파일들이 많이 있습니다. 자동으로 생성되기도 하고, 설정을 위해 사용자가 직접 작성하기도 합니다. 이런 파일들을 닷파일dotfiles이라고도 부릅니다.

커맨드라인를 활용하는 개발자들에게 닷파일을 관리하는 건 매우 중요한 과제입니다. 셸 설정부터 다양한 커맨드라인 프로그램들의 설정을 담고 있기 때문에 닷파일만 잘 관리해도 여러 머신 간에 거의 같은 커맨드라인 환경에서 작업을 하는 게 가능해집니다. 자신만의 설정 파일을 dotfiles라는 이름으로 공개해두기도 합니다. 44BITS에서도 Mackup이라는 프로그램으로 닷파일을 관리하는 방법을 소개한 적이 있으니 참고해주세요.

29. 명령어 검색 경로 $PATH와 프로그램 실행하기

지금까지 여러가지 명령어들을 사용해보았습니다. 그런데 이 명령어들은 어떤 원리로 실행되는 걸까요? 어떤 프로그램을 실행하려면 적절한 위치에 프로그램 바이너리가 있어야만합니다. 이 때 사용하는 환경변수가 바로 PATH입니다. 먼저 printenv를 사용해 $PATH를 출력해보겠습니다.

$ printenv PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

이 값에는 4개의 디렉터리 :로 구분되어있습니다. 예를 들어 printenv를 실행하면 printenv를 찾을 때까지 차례로 PATH 값의 디렉터리에서 바이너리를 찾습니다. 그렇다면 printenv는 어디에 있을까요?

which 명령어를 사용해 프로그램의 위치를 찾아볼 수 있습니다. printenv, which, cat 명령어가 있는 디렉터리를 찾아보겠습니다.

$ which printenv 
/usr/bin/printenv
$ which which
/usr/bin/which
$ which cat
/bin/cat

/usr/bin에 있네요. 이 디렉터리는 PATH의 두번째 위치에 있습니다. 즉, PATH에 printenv 바이너리가 있는 디렉터리가 있기 때문에 printenv를 실행할 수 있습니다. which의 출력결과도 PATH 값을 참조합니다.

그렇다면 PATH의 값을 삭제하면 어떻게 될까요? 직접 확인해보겠습니다.

$ unset PATH
$ printenv PATH
bash: printenv: No such file or directory
$ which printenv
bash: which: No such file or directory

printenvwhich를 찾지 못 해서 실행이 되질 않습니다. 이번에는 which가 있는 /usr/bin만 PATH에 추가하고서 다시 whichprintenv, which, cat의 위치를 찾아보겠습니다.

$ export PATH=/usr/bin
$ which printenv
/usr/bin/printenv
$ which which
/usr/bin/which
$ which cat

printenvwhich는 프로그램 위치를 찾아냅니다만, /bin 디렉터리에 있는 cat은 찾지 못 하고 빈값을 출력합니다. 실제로 cat은 실행이 되지 않습니다.

$ cat
bash: cat: command not found

PATH의 원리가 이해되시나요? 실행되어야할 것 같은 명령어가 실행되지 않는다면 먼저 현재 셸의 PATH 환경변수부터 확인해보시기 바랍니다. PATH에는 물론 기본 시스템 디렉터리 이외에 자신이 원하는 디렉터리를 추가해줄 수 도 있습니다. 앞서 확인한 것처럼 :를 구분자로 디렉터리들을 연결하면 됩니다.

PATH에 없는 바이너리나, 특정 경로에 있는 바이너리 파일을 실행하고 싶다면 어떻게 해야할까요? 바이너리의 경로 전체를 입력해주면됩니다. 예를 들어 cat은 실제로는 /bin/cat에 있습니다.

$ /bin/cat /etc/bashrc
# System-wide .bashrc file for interactive bash(1) shells.
...

잘 실행되는 것을 알 수 있습니다. 현재 디렉터리에 있는 바이너리 파일을 사용하는 경우에는 ./<프로그램>을 실행해줍니다. 이번에는 /tmp 디렉터리로 이동해서 cat 파일을 복사해온 다음 실행해보겠습니다.

$ cd /tmp
$ ./cat /etc/bashrc
$ /bin/cp /bin/cat .
$ ./cat /etc/bashrc
# System-wide .bashrc file for interactive bash(1) shells.

현재 디렉터리의 cat으로 출력이 잘 되는 것을 확인할 수 있습니다. 단, 주의해야할 점은 프로그램과 같은 디렉터리에 있더라도 경로 표현을 생략하면 실행이 되지 않는다는 점입니다. 경로 표현을 생략하고 실행가능한 경우는 PATH 디렉터리 아래에 해당 명령어가 있는 경우입니다.

30. 프로세스 종료 상태 코드(Exit status code)

화면 출력만 봐서는 프로세스가 정상 실행되었는지, 실패했는지 판단하는 것이 쉽지 않습니다. 화면에는 직접 출력되지 않지만 프로세스 실행 결과가 정상적으로 이루어졌는지를 알려주는 규약이 프로세스 종료 상태 코드(Exit status code)입니다. 셸에서는 방금 전에 실행된 프로세스의 종료 상태코드가 $? 변수에 저장됩니다.

$ cat exist.txt
exist
$ echo $?
0

0은 정상적으로 종료되었다는 의미입니다. 즉, 바로 앞에서 실행한 cat 명령어는 성공적으로 실행되었습니다.

이번에는 존재하지 않는 파일을 출력하려고 시도한 다음 종료 코드를 확인해보겠습니다.

$ cat not_exist.txt
cat: not_exist.txt: No such file or directory
$ echo $?
1

종료 코드가 1입니다. 1은 에러를 의미합니다. 종료 상태 코드는 0부터 255까지 값이 사용될 수 있습니다만 일반적으로 0은 성공, 이외의 값(non-zero status)은 실패로 처리합니다.

스크립팅을 하는 게 아니라면, 셸에서 종료 코드를 직접 사용할 일은 많지 않습니다만 개념을 알고 있으면 매우 유용합니다. CircleCI와 GitHub Actions와 같은 지속적 통합 도구에서도 특정 명령어가 실패하면 빌드가 중지되는 경우가 있는데, 이 때도 종료 코드값을 활용해 판단합니다.

31. 프로세스의 출력 결과를 다른 프로그램의 입력으로 연결해주는 파이프(|)

셸에서 매우 많이 활용되는 기법 중 하나가 바로 파이프입니다. 구체적으로 들어가면 조금 (많이) 복잡합니다만*, 여기서는 간단하게만 다루고 넘어가도록 하겠습니다. 앞서 STDIN을 통해서 어떤 입력이 프로세스에 전달되고 프로세스는 STDOUT과 STDERR에 출력한다는 걸 살펴보았습니다. 파이프를 사용하면 왼쪽 프로세스 출력 결과(STDOUT)를 오른쪽의 프로세스 입력(STDIN)으로 연결해줍니다.

* 파이프의 동작 원리에 대해서는 Roslyn Michelle Cyrus의 Pipes, Forks, & Dups: Understanding Command Execution and Input/Output Data Flow 글에 매우 상세히 정리되어있습니다.

파이프를 사용하기에 앞서 간단한 fruits.txt 예제 파일을 만들어보겠습니다.

$ echo 'banana' >> fruits.txt
$ echo 'apple' >> fruits.txt
$ echo 'kiwi' >> fruits.txt

sort를 사용하면 이 파일의 내용을 정렬할 수 있습니다.

$ cat fruits.txt
banana
apple
kiwi
$ sort fruits.txt
apple
banana
kiwi

라인 별로 알파벳순으로 정렬된 것을 확인할 수 있습니다.

sort는 파일을 인자로 받을 수 있을 뿐만 아니라, cat처럼 STDIN을 입력으로 받을 수도 있습니다. sort를 인자 없이 실행하면 입력대기 상태가 됩니다. 이 상태에서 앞의 fruits.txt 파일의 내용을 입력하고, ^D로 프로그램을 종료합니다. 그럼 그 아래에 STDOUT으로 라인 별로 정렬된 결과가 출력되는 것을 확인할 수 있습니다.

$ sort
banana
apple
kiwi

^D
apple
banana
kiwi

여기서 정리를 한 번 보겠습니다.

여기에 한 가지 양념을 쳐보겠습니다.

간단한 내용을 조금 빙 둘러설명한 느낌이 듭니다만, 이제 cat fruits.txt | sort를 실행하면 어떤 결과가 나올지 상상이 되시나요?

$ cat fruits.txt | sort
apple
banana
kiwi

cat fruits.txt의 출력은 |를 통해 sort 프로세스의 STDIN으로 전달되기 때문에 최종적으로 정렬이 된 내용만 화면에 출력됩니다. 파이프를 설명하기 전에 이미 앞에서 tee를 설명하기 위해서 파이프를 사용한 적이 있습니다.

$ echo 'Hi, world!' | tee sayhi.txt

어떻게 동작하는지 잠시 생각해보시기 바랍니다.

파이프는 여러 프로그램들의 기능을 조합해서 사용할 수 있기 때문에 활용도가 매우 높습니다. 예를 들어 env로 환경변수 목록을 가져와 grep으로 문자열 필터링을 하고, 그 결과를 다시 sort로 정렬하면 다음과 같은 결과를 얻을 수 있습니다.

$ env | grep TERM | sort
COLORTERM=truecolor
ITERM_PROFILE=Default
ITERM_SESSION_ID=w1t0p0:7AADB02A-E2C9-44B3-AF7B-9F14499C8F9C
LC_TERMINAL=iTerm2
LC_TERMINAL_VERSION=3.3.9
TERM=xterm-256color
TERM_PROGRAM=iTerm.app
TERM_PROGRAM_VERSION=3.3.9
TERM_SESSION_ID=w1t0p0:7AADB02A-E2C9-44B3-AF7B-9F14499C8F9C

파이프는 처음 보면 매우 헷갈리게 느껴집니다면, 사용하면 할 수록 셸의 강력함을 느끼게 해주는 기능 중 하나입니다.

32. 패키지 매니저: apt, yum, brew

맥OS에 앱스토어가 있는 것처럼 리눅스나 맥OS 커맨드라인 환경에도 패키지 매니저가 있습니다. 패키지 매니저로 프로그램을 쉽게 다운로드하고 설치할 수 있습니다. 맥OS에서는 홈브류Homebrew가 대표적입니다. 리눅스에서는, 우분투Ubuntu, 데비안Debian 계열에서는 apt, 레드 햇Red Hat, CentOS, 아마존 리눅스AmazonLinux 같은 계열에서는 yum을 사용합니다.

예를 들어 웹페이지를 다운로드 받을 수 있는 wget이라는 명령어가 있습니다. 패키지 매니저 명령어 하나면 설치하고 바로 사용해볼 수 있습니다.

# MacOS
$ brew install wget

# Debian, Ubuntu, ...
$ apt install wget

# Red Hat, CentOS, AmazonLinux, ...
$ yum install wget

이제 wget을 사용해볼 수 있습니다.*

* 이 글에서는 다루고 있지 않습니다만, 리눅스 환경에서 패키지 매니저를 사용하는 경우 시스템 전체에 영향을 줄 수 있기 때문에 반드시 관리자 권한이 필요합니다. 관리자 권한이 부여되어있는 사용자라면 명령어 앞에 sudo를 붙여서 실행할 수 있습니다.

$ wget 44bits.io -O 44bits.html
$ head 44bits.html
<!DOCTYPE html>
<html lang="ko">
...

패키지 매니저를 활용하면 놀라울 정도로 쉽게 프로그램을 설치하고 시스템을 확장할 수 있습니다.

맥OS의 홈브류에 대한 보다 자세한 정보는 아래 글을 참고해주세요.

마치며

커맨드라인 인터페이스가 어려운 이유는 고인물들에게는 너무나도 익숙한 관습과 전제들이 산재해있기 때문이라고 생각합니다. 글을 쓰다보니 제가 얼마나 많은 사전지식 위에서 작업하는지 한 번 더 깜짝 놀랐습니다. 처음에는 커맨드라인과 셸에 익숙해지기 위한 몇 가지 팁을 정리해보려고 했습니다만, 이렇게나 길어져버렸네요. 심지어 리눅스에 대한 얘기는 거의 다루지 않았는 데도 이 정도입니다. 부디 커맨드라인이 아직 어려우신 분들께 도움이 되기를 바랍니다.

여기서 다룬 내용은 이제 첫 걸음에 지나지 않습니다. 이 글에서는 일반적인 환경에 대응하기 위해 Bash 환경과 리눅스와 맥OS에서 기본적으로 사용할 수 있는 명령어들을 위주로 설명했습니다. 셸도 Bash 뿐만 아니라 다양한 셸들이 있습니다. 개인적으로는 Bash보다 조금 더 기본 기능이 풍부한 Zsh을 선호합니다. 셸도 설정하기 시작하면 한도 끝도 없습니다만, 최근에는 기본 설정이 충실한 Oh My Zsh을 사용하고 있습니다.

터미널도 OS의 기본 터미널만 있는 게 아닙니다. 맥OS에서는 iTerm2 가 실질적인 표준으로 사용됩니다. 이 글의 캡쳐도 대부분 iTerm2를 촬영하였습니다. 그 외에 리눅스에서 인기있었던 Terminator나 일렉트론 기반의 Hyper 같은 터미널 애플리케이션도 있습니다.

다수의 셸을 한 번에 사용할 수 있게 도와주는 screen과 tmux 같은 터미널 멀티플렉서라는 도구도 있습니다. 데스크탑의 터미널 애플리케이션을 사용하면 탭이나 화면 분할이 쉽게 가능하기 때문에 사용할 이유가 많지 않습니다. 하지만 SSH를 활용하는 서버 환경에서는 커맨드라인 인터페이스만으로 화면 분할을 할 수 있는 멀티플렉서가 매우 큰 도움이 됩니다. 또한 터미널 멀티플렉스들은 서버 방식으로 동작하기 때문에 오래 걸리는 작업을 유지해놓는 용도로도 자주 활용됩니다.

그리고 이 글에서는 탈출 방법만 소개하고 넘어갔습니다만, 이맥스(Emacs)나 빔(Vim)도 사실은 필수 스킬 중 하나입니다.

90년대 유모아: Emacs와 Vim의 학습 곡선

터미널과 셸을 비롯해 개발환경 셋업 하면, 커맨드라인 환경 셋업도 매우 큰 비중을 차지하니, 관심 있으신 분들은 다음 글들을 참고해보시기 바랍니다.

44BITS에서는 이외에도 커맨드라인 도구들을 꾸준히 소개하고 있습니다. 커맨드라인 인터페이스를 확장해주는 도구로는 단연 페코(Peco)가 으뜸입니다. 페코와 비슷한 용도로 활용할 수 있는 최준건 님이 만드신 fzf도 많은 인기를 얻고 있습니다.

다수의 프로젝트를 관리하는 경우 디렉터리 별로 개발환경을 셋업할 수 있게 해주는 direnv가 도움이 됩니다.

단순히 파일 출력을 넘어서, JSON 포맷을 출력하고 편집하는 용도로는 jq라는 프로그램이 많이 사용됩니다. 프론트엔드 개발자는 물론, API 포맷을 자주 다루는 백엔드 개발자 두루두루 활용도가 높습니다.

끝으로 이 글을 쓰면서도 많이 참고한 Bash 레퍼런스 매뉴얼을 남겨둡니다. 셸 환경에 대한 거의 모든 정보를 담고 있는 문서입니다. 여기있는 내용을 다 알 필요는 없고, 필요할 때 찾아보는 정도면 충분합니다. 제가 아는 내용도 레퍼런스 매뉴얼로 보면 극히 일부에 불과합니다.

그럼 커맨드라인을 향한 즐거운 여정이 되기를 기원합니다. 🙏