Home Linux Shell I/O and Command Flow
Post
Cancel

Linux Shell I/O and Command Flow

# Linux Shell I/O and Command Flow

Q&A: /dev/null에서 “출력을 버린다”는 의미

질문

/dev/null과 관련해서, “출력을 버린다”는 게 무슨 뜻인가? /dev/null을 붙이지 않는 것과 어떻게 다른가?

답변

명령어를 실행하면 결과가 보통 터미널 화면에 출력된다. 예를 들어 다음 명령을 실행하면 hello가 화면에 보인다.

1
echo "hello"

결과는 다음과 같다.

1
hello

그런데 출력을 /dev/null로 보내면 화면에 아무것도 보이지 않는다.

1
echo "hello" > /dev/null

여기서 >는 출력 방향을 바꾸는 리다이렉션(redirection) 이다. 원래는 터미널 화면으로 가야 할 출력이 /dev/null로 간다.

/dev/null은 받은 내용을 저장하지 않고 즉시 버리는 특수 파일이다. 그래서 “출력을 버린다”는 말은 명령어의 실행 결과를 화면에도 보여주지 않고, 파일에도 저장하지 않고, 그냥 없애 버린다는 뜻이다.

차이를 비교하면 다음과 같다.

1
echo "hello"
1
hello
1
echo "hello" > /dev/null
1
# 아무것도 출력되지 않음

중요한 점은 /dev/null을 붙인다고 명령어가 실행되지 않는 것은 아니라는 점이다. 명령어는 정상적으로 실행된다. 다만 그 명령어가 만들어낸 출력만 보이지 않게 된다.

예를 들어 다음 명령은 ls의 결과를 화면에 보여준다.

1
ls

하지만 다음 명령은 ls를 실행하되, 목록 출력은 버린다.

1
ls > /dev/null

이런 방식은 주로 “결과 내용은 필요 없고, 성공했는지 실패했는지만 알고 싶을 때” 사용한다.

1
ls /home/shin > /dev/null

이 명령은 /home/shin 디렉토리 목록을 화면에 보여주지 않는다. 하지만 명령이 성공했는지는 종료 상태(exit status)로 확인할 수 있다.

1
2
ls /home/shin > /dev/null
echo $?

0이 나오면 성공, 0이 아닌 값이 나오면 실패를 의미한다.

또 하나 중요한 구분이 있다. 기본적으로 >는 표준 출력(stdout) 만 /dev/null로 보낸다. 에러 메시지인 표준 에러(stderr) 는 그대로 화면에 보일 수 있다.

1
ls /not-exist > /dev/null

위 명령은 일반 출력은 버리지만, /not-exist가 없다는 에러 메시지는 화면에 보인다.

에러까지 모두 숨기려면 다음처럼 쓴다.

1
ls /not-exist > /dev/null 2>&1

정리하면 다음과 같다.

1
2
3
명령어만 실행              결과가 화면에 보인다
명령어 > /dev/null         일반 출력(stdout)을 버린다
명령어 > /dev/null 2>&1    일반 출력(stdout)과 에러(stderr)를 모두 버린다

따라서 /dev/null을 붙이지 않는 것과 붙이는 것의 차이는 명령어 실행 여부가 아니라, 출력 결과를 사용자에게 보여줄지 버릴지의 차이다.

Q&A: 표준 입출력, 리다이렉션, 파이프, 특수 기호

질문

stdout, stdin, stderr, >, |, >>, 2>&1 등의 용어에 대한 이해와 정리가 필요하다. 제시한 기호들 말고도 리눅스를 다룰 때 알아야 할 특수 기호나 용어들은 무엇인가?

답변

리눅스 명령어를 제대로 이해하려면 먼저 표준 스트림(standard streams) 을 알아야 한다. 명령어는 실행될 때 보통 입력을 받고, 결과를 내보내고, 에러를 내보낸다. 리눅스는 이것을 세 가지 기본 통로로 나누어 다룬다.

1
2
3
stdin   표준 입력   0번   명령어가 입력을 받는 통로
stdout  표준 출력   1번   명령어의 정상 결과가 나가는 통로
stderr  표준 에러   2번   에러 메시지가 나가는 통로

예를 들어 cat은 표준 입력을 받아 표준 출력으로 내보낼 수 있다.

1
cat

이 상태에서 사용자가 키보드로 입력하면, 그 입력이 stdin으로 들어가고 cat은 그것을 다시 stdout으로 출력한다.

가장 자주 쓰는 기호들은 다음과 같다.

1
2
3
4
5
6
7
8
>      stdout을 파일로 보낸다. 기존 파일 내용은 덮어쓴다.
>>     stdout을 파일 끝에 추가한다.
<      파일 내용을 stdin으로 넣는다.
|      왼쪽 명령의 stdout을 오른쪽 명령의 stdin으로 연결한다.
2>     stderr를 파일로 보낸다.
2>>    stderr를 파일 끝에 추가한다.
2>&1   stderr를 stdout이 가는 곳으로 함께 보낸다.
&>     stdout과 stderr를 함께 파일로 보낸다. Bash에서 주로 사용한다.

>는 출력 결과를 파일로 저장할 때 쓴다.

1
ls > files.txt

위 명령은 ls 결과를 화면에 보여주지 않고 files.txt에 저장한다. 이미 파일이 있으면 기존 내용은 덮어쓴다.

>>는 덮어쓰지 않고 파일 끝에 추가한다.

1
date >> log.txt

<는 파일을 명령어의 입력으로 넣는다.

1
sort < names.txt

|는 파이프(pipe)다. 왼쪽 명령어의 결과를 오른쪽 명령어의 입력으로 넘긴다.

1
ls -la | grep ".md"

위 명령은 ls -la의 출력 중 .md가 들어간 줄만 grep으로 걸러낸다.

stderr는 정상 출력과 별도의 통로다. 그래서 다음 명령에서 일반 출력은 버려도 에러는 화면에 보일 수 있다.

1
ls /not-exist > /dev/null

에러만 따로 파일에 저장하려면 다음처럼 쓴다.

1
ls /not-exist 2> error.log

일반 출력과 에러를 모두 같은 파일에 저장하려면 다음처럼 쓴다.

1
some-command > output.log 2>&1

여기서 2>&1은 “2번 통로인 stderr를 1번 통로인 stdout이 현재 가는 곳으로 보내라”는 뜻이다. 순서가 중요하다.

1
some-command > output.log 2>&1

이것은 stdout을 output.log로 보낸 뒤, stderr도 stdout이 가는 곳인 output.log로 보낸다.

반면 다음은 의미가 달라질 수 있다.

1
some-command 2>&1 > output.log

이 경우 stderr는 먼저 현재 stdout, 즉 화면으로 연결되고, 그 다음 stdout만 파일로 바뀐다. 그래서 에러는 화면에 남을 수 있다.

리눅스에서 자주 만나는 다른 특수 기호와 용어도 함께 정리하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.        현재 디렉토리
..       부모 디렉토리
~        현재 사용자의 홈 디렉토리
/        파일 시스템 최상위 디렉토리
*        여러 글자를 의미하는 와일드카드
?        한 글자를 의미하는 와일드카드
[]       문자 범위 또는 후보 지정
;        앞 명령이 끝나면 다음 명령 실행
&&       앞 명령이 성공하면 다음 명령 실행
||       앞 명령이 실패하면 다음 명령 실행
&        명령을 백그라운드에서 실행
$?       직전 명령의 종료 상태
$VAR     변수 값 참조
$(...)   명령 실행 결과를 문자열로 치환
`...`    오래된 명령 치환 문법. 보통 $(...)를 권장한다.
\        다음 문자의 특수 의미를 제거하거나 줄바꿈을 이어준다.
'...'    문자열을 그대로 보존한다. 변수 치환이 일어나지 않는다.
"..."    대부분 보존하지만 변수 치환은 일어난다.

예를 들어 .과 ..은 경로에서 매우 자주 쓰인다.

1
2
cd .
cd ..

cd .은 현재 디렉토리로 이동한다는 뜻이라 실제 위치가 바뀌지 않는다. cd ..은 부모 디렉토리로 이동한다.

*는 여러 파일을 한 번에 지정할 때 쓴다.

1
ls *.md

현재 디렉토리에서 .md로 끝나는 파일들을 보여준다.

&&는 앞 명령이 성공했을 때만 뒤 명령을 실행한다.

1
mkdir backup && cp *.md backup/

mkdir backup이 성공해야 cp *.md backup/이 실행된다.

||는 앞 명령이 실패했을 때 뒤 명령을 실행한다.

1
ls missing.txt || echo "파일이 없습니다"

$?는 직전 명령의 성공 여부를 확인할 때 쓴다.

1
2
ls /home
echo $?

보통 0은 성공, 0이 아닌 값은 실패를 의미한다.

$(...)는 명령 실행 결과를 다른 명령 안에 넣을 때 쓴다.

1
echo "오늘 날짜는 $(date) 입니다"

작은따옴표와 큰따옴표의 차이도 중요하다.

1
2
3
name="shin"
echo '$name'
echo "$name"

첫 번째 출력은 $name 그대로 나오고, 두 번째 출력은 변수 값인 shin이 나온다.

마지막으로 리눅스 명령어 조합의 핵심은 다음 흐름으로 이해하면 좋다.

1
2
입력(stdin) -> 명령어 실행 -> 정상 출력(stdout)
                         -> 에러 출력(stderr)

그리고 리다이렉션과 파이프는 이 통로의 방향을 바꾸는 도구다.

1
2
3
4
5
6
>       stdout을 파일로 보낸다
>>      stdout을 파일에 추가한다
<       파일을 stdin으로 넣는다
|       stdout을 다음 명령의 stdin으로 연결한다
2>      stderr를 파일로 보낸다
2>&1    stderr를 stdout과 같은 곳으로 보낸다

즉 리눅스의 특수 기호들은 단순한 암기 대상이 아니라, 입력과 출력의 흐름을 조작하는 문법으로 이해해야 한다.

Q&A: 백그라운드 실행, /dev/null, 표준 입출력, 파일처럼 다루기

질문

백그라운드, 포그라운드 실행의 개념과 /dev/null로 출력을 버리는 개념은 어떻게 다른가? 이것은 “모든 것을 파일처럼 다룬다”는 리눅스 철학과 관련이 있는가? 리눅스에서 명령어의 출력은 기본적으로 터미널 화면으로 가는가? 그렇다면 입력은 /bin의 특정 명령어 파일로 가는가?

답변

먼저 개념을 분리해야 한다. 포그라운드/백그라운드 실행은 “프로세스를 터미널과 어떤 관계로 실행할 것인가”의 문제이고, /dev/null로 출력을 버리는 것은 “명령어의 출력 통로를 어디로 보낼 것인가”의 문제다.

즉 둘은 서로 다른 축이다.

1
2
포그라운드/백그라운드 = 실행 방식의 문제
/dev/null 리다이렉션   = 출력 목적지의 문제

포그라운드 실행은 명령어가 터미널의 입력을 차지한 상태로 실행되는 것이다.

1
sleep 10

위 명령을 실행하면 10초 동안 터미널은 그 명령이 끝나기를 기다린다. 이 동안 사용자는 같은 터미널에서 다음 명령을 입력하기 어렵다. 이것이 포그라운드 실행이다.

백그라운드 실행은 명령어를 뒤에서 실행시키고, 터미널은 바로 다음 명령을 받을 수 있게 하는 방식이다.

1
sleep 10 &

여기서 &는 명령어를 백그라운드에서 실행하라는 뜻이다. 중요한 점은 백그라운드로 실행한다고 해서 출력이 자동으로 사라지는 것은 아니라는 점이다.

예를 들어 다음 명령은 백그라운드에서 실행되지만, 출력은 여전히 터미널에 나타날 수 있다.

1
some-command &

반대로 다음 명령은 포그라운드에서 실행되지만, 출력은 /dev/null로 버린다.

1
some-command > /dev/null

그리고 다음처럼 둘을 함께 쓸 수도 있다.

1
some-command > /dev/null 2>&1 &

이 명령은 다음 뜻을 가진다.

1
2
3
4
some-command        명령어를 실행한다
> /dev/null         정상 출력을 버린다
2>&1                에러 출력도 정상 출력과 같은 곳으로 보낸다
&                   백그라운드에서 실행한다

따라서 백그라운드 실행과 /dev/null은 역할이 다르다.

1
2
3
command &                 백그라운드에서 실행하지만 출력은 보일 수 있다
command > /dev/null       포그라운드에서 실행하지만 출력은 버린다
command > /dev/null &     백그라운드에서 실행하고 출력도 버린다

이제 “모든 것을 파일처럼 다룬다”는 리눅스 철학과의 관련을 보자.

리눅스에서는 많은 대상을 파일처럼 다룬다. 일반 파일뿐 아니라 디렉토리, 장치, 터미널, 파이프, 소켓도 파일과 비슷한 인터페이스로 다룬다. 정확히는 프로그램 입장에서 read, write, open, close 같은 방식으로 다룰 수 있게 만든다는 뜻에 가깝다.

/dev/null은 이 철학과 직접 관련이 있다. /dev/null은 실제 문서 파일이 아니라 장치 파일이다. 하지만 프로그램은 여기에 일반 파일처럼 출력할 수 있다.

1
echo "hello" > /dev/null

프로그램 입장에서는 /dev/null에 hello를 쓴다. 하지만 /dev/null 장치는 받은 데이터를 저장하지 않고 버린다.

터미널도 비슷하다. 우리가 보는 터미널 화면도 리눅스 내부에서는 장치 파일과 연결되어 있다. 예를 들어 현재 터미널은 보통 /dev/pts/0, /dev/pts/1 같은 장치 파일로 표현될 수 있다.

1
tty

이 명령을 실행하면 현재 터미널 장치 파일 경로가 나온다.

이제 표준 입출력으로 돌아가자. 리눅스에서 명령어의 출력은 기본적으로 어디로 가는가? 보통은 현재 터미널의 stdout으로 간다. 그래서 사용자는 결과를 화면에서 본다.

1
echo "hello"

이 명령의 출력은 기본적으로 현재 터미널로 간다.

하지만 엄밀히 말하면 출력이 “화면”으로 간다기보다, 프로세스의 stdout이 현재 터미널 장치에 연결되어 있다고 보는 편이 더 정확하다. 사용자가 터미널에서 명령어를 실행하면 셸이 새 프로세스를 만들고, 그 프로세스의 stdin/stdout/stderr를 현재 터미널에 연결해 준다.

기본 연결은 보통 다음과 같다.

1
2
3
stdin   -> 현재 터미널 키보드 입력
stdout  -> 현재 터미널 화면 출력
stderr  -> 현재 터미널 화면 출력

여기서 사용자가 반드시 바로잡아야 할 부분이 있다. 입력은 /bin의 명령어 파일로 가는 것이 아니다.

/bin/ls, /bin/cat 같은 것은 실행 가능한 프로그램 파일이다. 사용자가 명령어를 입력하면 셸은 그 파일을 찾아 실행한다. 하지만 실행된 뒤의 입력은 그 프로그램 파일 자체로 들어가는 것이 아니라, 실행 중인 프로세스의 stdin으로 들어간다.

예를 들어 다음 명령을 보자.

1
cat

셸은 cat 실행 파일을 찾아 프로세스로 실행한다. 그 후 사용자가 키보드로 입력한 내용은 /bin/cat 파일로 들어가는 것이 아니라, 실행 중인 cat 프로세스의 stdin으로 들어간다. cat 프로세스는 stdin에서 읽은 내용을 stdout으로 다시 출력한다.

흐름은 다음과 같다.

1
키보드 입력 -> 터미널 -> cat 프로세스의 stdin -> cat 실행 -> cat 프로세스의 stdout -> 터미널 화면

반면 ls는 보통 stdin을 거의 사용하지 않는다.

1
ls

ls는 현재 디렉토리 정보를 읽고 그 결과를 stdout으로 출력한다. 사용자가 키보드로 내용을 입력해서 ls에게 넘기는 구조가 아니다.

파일을 stdin으로 넣고 싶다면 <를 사용한다.

1
cat < names.txt

이 경우 흐름은 다음과 같다.

1
names.txt 파일 내용 -> cat 프로세스의 stdin -> cat 프로세스의 stdout -> 터미널 화면

파이프를 사용하면 왼쪽 명령의 stdout이 오른쪽 명령의 stdin으로 연결된다.

1
ls | grep ".md"

흐름은 다음과 같다.

1
ls 프로세스의 stdout -> grep 프로세스의 stdin -> grep 프로세스의 stdout -> 터미널 화면

정리하면 다음과 같다.

1
2
3
4
5
6
7
8
/bin/ls, /bin/cat        실행 가능한 명령어 파일
실행 중인 ls, cat        프로세스
stdin                   프로세스가 입력을 받는 통로
stdout                  프로세스가 정상 출력을 내보내는 통로
stderr                  프로세스가 에러 출력을 내보내는 통로
터미널                  기본 stdin/stdout/stderr가 연결되는 대상
/dev/null               출력을 보내면 버리는 장치 파일
&                       프로세스를 백그라운드로 실행하는 기호

따라서 올바른 이해는 다음과 같다.

1
2
3
4
5
6
명령어 파일은 실행의 대상이다.
입력은 명령어 파일로 가는 것이 아니라, 실행 중인 프로세스의 stdin으로 간다.
출력은 실행 중인 프로세스의 stdout/stderr에서 나온다.
기본적으로 stdin/stdout/stderr는 현재 터미널에 연결된다.
리다이렉션과 파이프는 이 연결을 바꾸는 문법이다.
백그라운드 실행은 프로세스를 터미널 뒤쪽에서 계속 실행시키는 방식이다.

Q&A: stdin은 왜 필요한가? cat < names.txt와 cat names.txt의 차이

질문

stdin은 왜 굳이 사용하는가? cat < names.txt와 cat names.txt는 차이가 없어 보인다.

답변

겉으로 보이는 출력만 보면 다음 두 명령은 거의 같다.

1
2
cat names.txt
cat < names.txt

둘 다 names.txt의 내용을 화면에 출력한다. 하지만 내부 동작 방식은 다르다.

1
cat names.txt

이 명령은 names.txt라는 파일 이름을 cat 명령어의 인자(argument)로 넘긴다. 그러면 cat 프로그램이 직접 names.txt 파일을 열어서 읽는다.

반면 다음 명령은 다르다.

1
cat < names.txt

여기서는 셸이 먼저 names.txt 파일을 열고, 그 파일 내용을 cat 프로세스의 stdin에 연결한다. cat 입장에서는 파일 이름을 받은 것이 아니라, 그냥 stdin에서 들어오는 데이터를 읽는 것이다.

정리하면 다음과 같다.

1
2
cat names.txt    cat이 파일 이름을 받아 직접 파일을 연다
cat < names.txt  셸이 파일을 열어 cat의 stdin으로 넣어준다

cat에서는 결과가 같아 보이지만, stdin이 중요한 이유는 모든 명령어가 파일 이름 인자를 받는 것은 아니기 때문이다. 어떤 프로그램은 파일 이름을 직접 받지 않고 stdin으로 들어오는 데이터만 처리하도록 설계될 수 있다.

예를 들어 파이프는 stdin/stdout이 있기 때문에 가능하다.

1
cat names.txt | grep "kim"

흐름은 다음과 같다.

1
cat의 stdout -> grep의 stdin

grep은 왼쪽 명령어의 결과를 파일이 아니라 stdin으로 받는다. 이것이 리눅스 명령어 조합의 핵심이다.

또 stdin은 파일이 아닌 입력도 같은 방식으로 처리하게 해준다. 예를 들어 사용자가 직접 키보드로 입력할 수도 있다.

1
cat

이 상태에서는 키보드 입력이 cat의 stdin으로 들어간다. cat은 stdin으로 들어온 내용을 stdout으로 다시 출력한다.

또 다른 명령어의 출력도 stdin으로 받을 수 있다.

1
ls | sort

여기서 sort는 파일 이름을 받은 것이 아니라 ls의 출력 결과를 stdin으로 받아 정렬한다.

즉 stdin은 단순히 파일을 읽기 위한 기능이 아니다. stdin의 진짜 목적은 입력의 출처를 통일하는 것이다.

1
2
3
키보드 입력
파일 내용
다른 명령어의 출력

이 모든 것을 프로그램 입장에서는 stdin이라는 하나의 통로로 받을 수 있다.

그래서 다음 세 가지는 입력 출처는 다르지만, 프로그램 입장에서는 모두 “stdin에서 읽는다”는 공통된 방식으로 처리할 수 있다.

1
2
3
cat
cat < names.txt
printf "kim\nlee\npark\n" | cat

cat names.txt와 cat < names.txt가 비슷해 보이는 이유는 cat이 파일 인자와 stdin 입력을 모두 지원하기 때문이다. 하지만 리눅스의 더 큰 관점에서는 stdin이 있어야 명령어들을 자유롭게 연결할 수 있다.

좋은 이해는 다음과 같다.

1
2
3
4
파일 인자     특정 프로그램에게 파일 이름을 직접 넘기는 방식
stdin         프로그램에게 데이터 흐름을 넘기는 공통 입력 통로
파이프 |      한 프로그램의 stdout을 다른 프로그램의 stdin에 연결하는 장치
리다이렉션 <  파일 내용을 프로그램의 stdin에 연결하는 장치

따라서 cat 예시만 보면 stdin이 불필요해 보일 수 있지만, 리눅스 전체에서는 stdin이 명령어 조합과 자동화의 핵심이다.

Q&A: 여러 단계의 명령을 한꺼번에 입력하는 방식

질문

복수 단계의 명령을 한꺼번에 입력하는 방식은 무엇인가? Enter를 치거나, 한 줄로 쓰거나 하는 방식이 있는 것으로 안다. 다양한 방식의 예시와 원리를 알고 싶다. 예를 들어 cmd, PowerShell, shell에서 입력하는 엔터 방식이 모두 다르다.

답변

여러 단계의 명령을 한꺼번에 입력하는 방식은 크게 두 가지 관점으로 나눌 수 있다.

1
2
1. 명령어 여러 개를 어떤 관계로 이어서 실행할 것인가
2. 한 명령어를 여러 줄에 나누어 입력할 것인가

첫 번째는 ;, &&, ||, | 같은 명령 연결 방식의 문제다. 두 번째는 \, 따옴표, 괄호, here document 같은 줄 계속 입력 방식의 문제다.

1. 명령어를 순서대로 실행하기: ;

;는 앞 명령의 성공/실패와 관계없이 다음 명령을 실행한다.

1
mkdir logs; cd logs; touch app.log

이 명령은 다음을 한 줄에 쓴 것이다.

1
2
3
mkdir logs
cd logs
touch app.log

다만 ;는 앞 명령이 실패해도 뒤 명령을 실행한다. 그래서 실패하면 멈춰야 하는 작업에는 적합하지 않을 수 있다.

2. 성공했을 때만 다음 명령 실행하기: &&

&&는 앞 명령이 성공했을 때만 뒤 명령을 실행한다.

1
mkdir logs && cd logs && touch app.log

이 경우 mkdir logs가 실패하면 cd logs는 실행되지 않는다. cd logs가 실패하면 touch app.log도 실행되지 않는다.

실무에서는 ;보다 &&가 더 안전한 경우가 많다.

1
2
;   앞 명령 실패와 관계없이 계속 실행
&&  앞 명령 성공 시에만 계속 실행

3. 실패했을 때만 다음 명령 실행하기: ||

||는 앞 명령이 실패했을 때만 뒤 명령을 실행한다.

1
ls missing.txt || echo "파일이 없습니다"

missing.txt가 없어서 ls가 실패하면 echo가 실행된다.

&&와 ||를 함께 쓰면 간단한 조건 처리처럼 사용할 수 있다.

1
grep "ERROR" app.log && echo "에러 있음" || echo "에러 없음"

다만 이런 방식은 명령의 성공/실패 상태에 의존하므로, 복잡한 로직은 shell script의 if문으로 쓰는 편이 더 명확하다.

4. 왼쪽 출력을 오른쪽 입력으로 넘기기: |

파이프 |는 왼쪽 명령의 stdout을 오른쪽 명령의 stdin으로 연결한다.

1
ls -la | grep ".md"

흐름은 다음과 같다.

1
ls -la의 stdout -> grep의 stdin -> grep의 stdout -> 터미널

여러 단계로 이어 붙일 수도 있다.

1
cat app.log | grep "ERROR" | sort | uniq

하지만 grep은 파일 이름을 직접 받을 수 있으므로, 아래처럼 쓰는 편이 더 간단한 경우도 많다.

1
grep "ERROR" app.log | sort | uniq

5. 한 명령어를 여러 줄로 나누기: \

Bash 같은 리눅스 셸에서는 줄 끝에 \를 붙이면 다음 줄까지 하나의 명령으로 이어진다.

1
2
3
4
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"shin"}' \
  https://example.com/users

여기서 \는 “아직 명령이 끝나지 않았다. 다음 줄도 이어서 읽어라”는 뜻이다.

주의할 점은 \ 뒤에 공백이 있으면 문제가 될 수 있다는 것이다. 줄 끝의 마지막 문자가 \여야 한다.

6. 따옴표가 닫히지 않으면 계속 입력 상태가 된다

셸에서 따옴표를 열고 닫지 않으면 Enter를 쳐도 명령이 실행되지 않고 다음 줄 입력을 기다린다.

1
2
echo "hello
world"

셸은 큰따옴표가 닫힐 때까지 계속 입력을 받는다. 보통 프롬프트가 >처럼 바뀐다.

1
2
$ echo "hello
> world"

이 >는 리다이렉션 기호가 아니라, 셸이 “아직 입력이 끝나지 않았다”고 보여주는 보조 프롬프트다.

7. 괄호나 블록을 사용해 여러 명령 묶기

소괄호 (...)는 명령 묶음을 서브셸에서 실행한다.

1
(cd logs && ls -la)

이 경우 괄호 안에서 cd logs를 해도, 괄호 밖 현재 디렉토리는 바뀌지 않는다.

중괄호 { ...; }는 현재 셸에서 명령을 묶는다.

1
{ echo "start"; date; echo "end"; }

주의할 점은 { 뒤와 } 앞에 공백이 필요하고, 마지막 명령 뒤에 ;가 필요하다는 것이다.

1
2
(...)  서브셸에서 실행
{ ...; } 현재 셸에서 실행

8. 여러 줄 입력을 명령어에 넘기기: here document

here document는 여러 줄 텍스트를 명령어의 stdin으로 넘길 때 사용한다.

1
2
3
4
cat << EOF
hello
linux
EOF

흐름은 다음과 같다.

1
hello\nlinux 텍스트 -> cat의 stdin -> cat의 stdout -> 터미널

파일을 만들 때도 자주 쓴다.

1
2
3
4
cat > memo.txt << EOF
first line
second line
EOF

이 방식은 스크립트에서 설정 파일이나 긴 입력을 만들 때 유용하다.

9. Bash에서 Enter가 실행인지 계속 입력인지 결정되는 원리

Bash에서 Enter를 쳤을 때 명령이 실행되는지는 셸이 보기에 문장이 완성되었는지에 따라 달라진다.

즉 다음 상태라면 계속 입력을 기다린다.

1
2
3
4
5
따옴표가 닫히지 않음
괄호가 닫히지 않음
파이프 뒤에 다음 명령이 없음
줄 끝이 \ 로 끝남
here document가 아직 끝나지 않음

예를 들어 파이프 뒤에서 줄을 바꿔도 된다.

1
2
ls -la |
grep ".md"

셸은 | 뒤에 오른쪽 명령이 필요하다는 것을 알기 때문에 바로 실행하지 않고 다음 줄을 기다린다.

10. cmd.exe에서는 어떻게 다른가

Windows의 cmd.exe에서는 명령 연결 방식이 일부 비슷하지만 문법이 다르다.

dir & echo done

&는 앞 명령 성공/실패와 관계없이 다음 명령을 실행한다. Bash의 ;와 비슷하다.

dir && echo success

앞 명령이 성공하면 다음 명령을 실행한다.

dir missing || echo failed

앞 명령이 실패하면 다음 명령을 실행한다.

cmd에서 한 명령을 다음 줄로 이어 쓰려면 줄 끝에 ^를 사용한다.

echo hello ^
world

즉 Bash의 \ 역할을 cmd에서는 ^가 한다고 보면 된다.

11. PowerShell에서는 어떻게 다른가

PowerShell은 객체 기반 셸이라 Bash/cmd와 사고방식이 조금 다르다. 그래도 여러 명령 연결 방식은 있다.

PowerShell 7 이상에서는 &&, ||를 지원한다.

1
2
Get-ChildItem && Write-Output "success"
Get-ChildItem missing || Write-Output "failed"

명령을 한 줄에 여러 개 쓰려면 ;를 사용한다.

1
New-Item -ItemType Directory logs; Set-Location logs; New-Item app.log

PowerShell에서 파이프 |는 텍스트 줄만 넘기는 것이 아니라, 가능하면 객체를 넘긴다.

1
Get-ChildItem | Where-Object { $_.Extension -eq ".md" }

Bash의 파이프는 보통 텍스트 스트림을 넘긴다. PowerShell의 파이프는 객체를 넘긴다는 점이 큰 차이다.

PowerShell에서 줄을 이어 쓰는 대표적인 방법은 백틱 ` 이다.

1
2
3
Get-ChildItem `
  -Path . `
  -Recurse

하지만 PowerShell에서는 파이프 뒤, 괄호 안, 배열 안처럼 문장이 아직 끝나지 않았다는 것이 명확하면 백틱 없이도 줄바꿈할 수 있다.

1
2
3
Get-ChildItem |
  Where-Object { $_.Extension -eq ".md" } |
  Select-Object Name

12. Bash, cmd, PowerShell 비교

1
2
3
4
5
6
7
8
9
목적                       Bash/sh        cmd.exe        PowerShell
순차 실행                  ;              &              ;
성공 시 다음 실행           &&             &&             &&
실패 시 다음 실행           ||             ||             ||
파이프                     |              |              |
한 명령 줄 이어쓰기         \              ^              `
출력 덮어쓰기              >              >              >
출력 추가                  >>             >>             >>
에러 리다이렉션             2>             2>             2>

단, 같은 기호라도 셸마다 세부 의미가 다를 수 있다. 특히 PowerShell의 파이프는 텍스트보다 객체 중심이라는 차이가 크다.

13. 실전에서 추천하는 사용 방식

간단히 여러 명령을 실행할 때는 다음처럼 쓴다.

1
pwd; ls; date

앞 명령이 성공해야 다음 명령을 실행해야 한다면 &&를 쓴다.

1
mkdir logs && cd logs && touch app.log

출력을 단계적으로 가공해야 한다면 파이프를 쓴다.

1
grep "ERROR" app.log | sort | uniq

긴 명령은 \로 여러 줄에 나누어 쓴다.

1
2
3
4
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name":"shin"}' \
  https://example.com/users

반복해서 사용할 명령 묶음이 길어진다면 한 줄에 억지로 쓰기보다 .sh 파일로 분리하는 것이 좋다.

1
2
3
4
#!/usr/bin/env bash
mkdir -p logs
cd logs || exit 1
touch app.log

정리하면 다음과 같다.

1
2
3
4
5
6
7
8
;       순서대로 실행하되 실패해도 계속 간다
&&      성공했을 때만 다음 단계로 간다
||      실패했을 때만 다음 단계로 간다
|       출력과 입력을 연결한다
\       Bash에서 한 명령을 다음 줄로 이어 쓴다
^       cmd에서 한 명령을 다음 줄로 이어 쓴다
`       PowerShell에서 한 명령을 다음 줄로 이어 쓴다
Enter   문장이 완성되면 실행, 완성되지 않았으면 계속 입력

핵심은 Enter 자체가 항상 실행을 의미하는 것은 아니라는 점이다. 셸은 사용자가 입력한 문장이 문법적으로 완성되었는지 판단하고, 완성되었으면 실행하고, 아직 덜 끝났으면 다음 줄 입력을 기다린다.

Q&A: file descriptor 0, 1, 2는 무엇인가?

질문

file descriptor 0, 1, 2는 무엇인가?

답변

file descriptor는 프로세스가 파일, 터미널, 소켓, 파이프 같은 입출력 대상을 다룰 때 사용하는 작은 번호다. 리눅스에서는 “파일처럼 다룬다”는 철학 때문에 일반 파일뿐 아니라 네트워크 소켓과 터미널도 file descriptor로 다룰 수 있다.

기본으로 열려 있는 대표 번호가 0, 1, 2다.

1
2
3
0  stdin   표준 입력
1  stdout  표준 출력
2  stderr  표준 에러

예를 들어 터미널에서 프로그램을 실행하면 보통 다음처럼 연결된다.

1
2
3
0(stdin)   -> 키보드/터미널 입력
1(stdout)  -> 터미널 화면
2(stderr)  -> 터미널 화면

리다이렉션은 이 번호의 연결을 바꾸는 문법이다.

1
2
3
command > out.log
command 2> error.log
command > out.log 2>&1

각각 다음 뜻이다.

1
2
3
> out.log        stdout(1)을 파일로 보냄
2> error.log     stderr(2)를 파일로 보냄
2>&1             stderr(2)를 stdout(1)이 가는 곳으로 보냄

정리하면 다음과 같다.

1
2
file descriptor = 프로세스가 입출력 대상을 가리키는 번호
0, 1, 2         = 모든 CLI 프로그램 이해의 기본 입출력 번호

Q&A: epoll과 select는 무엇인가?

질문

epoll과 select는 무엇인가?

답변

select와 epoll은 여러 file descriptor 중에서 지금 읽거나 쓸 준비가 된 것을 기다리는 방법이다. 주로 네트워크 서버가 많은 클라이언트 연결을 동시에 다룰 때 필요하다.

서버가 클라이언트 1명만 상대한다면 단순히 하나의 소켓에서 읽으면 된다. 하지만 채팅 서버나 웹 서버처럼 연결이 많으면 매 연결마다 무작정 기다리는 방식은 비효율적이다.

1
2
3
socket A  데이터 도착
socket B  아직 없음
socket C  데이터 도착

이때 select나 epoll은 커널에게 “이 소켓들 중 준비된 것이 생기면 알려줘”라고 맡기는 방식이다.

1
2
3
4
select/epoll
-> 여러 fd 감시
-> 준비된 fd만 알려줌
-> 프로그램이 해당 fd를 읽거나 씀

차이는 대략 다음과 같다.

1
2
select  오래된 방식. 감시할 fd 목록을 매번 넘기고 규모가 커지면 비효율적
epoll   리눅스에서 대규모 연결을 더 효율적으로 감시하기 위한 방식

nginx, Redis, Node.js 같은 서버 프로그램이 많은 연결을 처리할 수 있는 배경에는 이런 이벤트 기반 I/O 모델이 있다.

정리하면 다음과 같다.

1
2
3
fd             입출력 대상 번호
select/epoll   여러 fd 중 이벤트가 생긴 대상을 기다리는 커널 기능
이벤트 루프     준비된 I/O 이벤트를 받아 처리하는 프로그램 구조
This post is licensed under CC BY 4.0 by the author.