- 3장에서는 터미널, 즉 명령행 인터페이스(CLI)를 사용할 수 있는 셸을 이용해 리눅스와 상호 작용하는 데 중점을 둔다.
기본 개요
- 다양한 옵션과 구성을 알아보기 전에 터미널이나 셸 같은 몇 가지 기본 용어에 집중해본다.
terminal
- 터미널은 텍스트로 된 사용자 인터페이스를 제공하는 프로그램이다.
- 즉 터미널은 키보드에서 문자를 읽어 화면에 표시하는 기능을 지원한다. 몇 년 전만 해도 이들은 통합 장치였으나 요즘의 터미널은 그저 앱일 뿐이다.
- 기본적인 문자 중심의 입력과 출력 외에도 터미널은 커서, 화면 처리, 그리고 잠재적으로 색상 지원이 가능하도록 이른바 이스케이프 시퀀스 또는 이스케이프 코드를 지원한다.
shell
- 터미널 내부에서 실행되며 명령 인터프리터 역할을 하는 프로그램
- 셸은 스트림을 통해 입력, 출력을 처리하고, 변수를 지원하며, 사용 가능한 내장 명령이 몇 가지 있으며, 명령 실행 및 상태를 처리하고, 일반적으로 대화식 사용과 스트립트 사용을 모두 지원한다.
- 공식적으로 셸은
sh
로 정의하며, 종종 POSIX shell라는 용어가 사용되는데 이는 스크립트와 이식성 맥락에서 중요하다. - 최근에는 대부분 bash shell이 기본적으로 널리 쓰이고 있다.
스트림
- 입력 스트림과 출력 스트림, 줄여서 I/O 주제부터 시작한다.
- 셸은 입력과 출력을 위한 세 가지 기본 파일 디스크립터(File Descriptor, FD)를 모든 프로세스에 제공한다.
stdin
(FD 0)stdout
(FD 1)stderr
(FD 2)
- 이 FD들은 기본적으로 화면과 키보드에 각각 연결되어 있다.
- 셸이 제공하는 기본값을 사용하지 않으려면 스트림을 재지정(redirect)할 수 있다.
$FD>
,<$FD
를 사용해 프로세스의 출력 스트림을 재지정할 수 있다.- 예를 들어
2>
는 stderr 스트림을 재지정한다는 의미이다. - stdout과 stderr를 모두 재지정하려면
&>
를 사용하면 된다. - 스트림을 제거하려면
/dev/null
을 사용하면 된다.
- 셸은 일반적으로 다름과 같은 여러 특수 문자를 이해한다.
&
(앰퍼샌드) : 명령 마지막에 배치되며 백그라운드에서 명령을 실행한다.\
(백 슬래시) : 긴 명령의 가독성을 높이기 위해 다음 행에서 명령을 계속할 때 사용한다.|
(파이프) : 한 프로세스의 stdout 값을 다음 프로세스의 stdin과 연결해 데이터를 파일에 임시로 저장하지 않고 바로 전달할 수 있다.
example : curl을 통한 html 콘텐츠 다운로드
1
2
3
4
5
6
7
8
curl https://example.com &> /dev/null
curl https://example.com > /tmp/content.txt 2> /tmp/curl-status # 출력값과 상태값을 다른 파일로 저장
head -3 /tmp/content.txt
cat /tmp/curl-status
cat > /tmp/interactive-input.txt # 대화식으로 값을 입력하고 파일에 저장
tr < /tmp/curl-status [A-Z][a-z] # tr 명령어를 사용해 모든 단어를 소문자로 만듬
변수
- 변수는 두 종류로 나뉜다
- 환경변수 : 셸 전체의 설정
env
명령어로 목록을 나열한다. - 셸 변수 : 현재 실행 상황에서 유효하다. 배시에서
set
명령어로 목록을 나열할 수 있다. 하위 프로세스는 셸 변수를 상속하지 않는다.
- 환경변수 : 셸 전체의 설정
- bash에서
export
명령어를 사용해 환경변수를 만들 수 있다. 변수의 값에 접근하고 싶을 때는 앞에$
를 붙이고, 변수를 제고하고 싶을 때는unset
을 사용한다.
1
2
set MY_VAR=42
set | grep MY_VAR
종료 상태
- 셸은 종료 상태(exit status) 라고 하는 것을 사용해 명령 실행 완료를 명령 호출자에게 알린다.
- 일반적으로 리눅스 명령은 종료될 때 상태를 반환한다. 이는 정상적인 종료(원활한 경로 혹은 happy path)일 수도 비정상 종료 일 수도 있다.
- 종료 상태값 0은 명령이 오류 없이 성공적으로 실행됐음을 의미하는 반면 1~255 사이의 값은 실패를 나타낸다.
- 종료 상태를 확인하려면
echo $?
를 사용한다.
- 종료 상태를 확인하려면
- 일부 셸에서는 마지막 상태값만 사용할 수 있으므로 파이프 사용 시 종료 상태 처리에 주의해야한다.
$PIPESTATUS
를 사용하면 이러한 제약사항을 해결할 수 있다.
내장 명령어
- 셸에는 여러 내장 명령어(built-in-command)가 있다.
help
명령을 사용하면 내장 명령어 목록을 나열할 수 있다.- 그러나 그 외 모든 것은 보통 /usr/bin(사용자 명령의 경우)이나 /usr/sbin(관리 명령의 경우)에 있는 셸 외부 프로그램이라는 점을 기억하자.
작업 제어
- 작업 제어는 대부분의 셸이 지원하는 기능이다.
- 기본적으로 명령을 입력하면 그 명령은 일반적으로 화면과 키보드를 제어하며, 이를 포어그라운드에서 실행된다고 한다.
- 프로세스를 백그라운드에서 시작하려면 명령 마지막에
&
를 넣고, 포어그라운드 프로세스를 백그라운드로 보내려면 Ctrl + Z를 누르면 된다. - 셸을 닫은 후에도 백그라운드 프로세스를 계속 실행하려면 nohup 명령을 앞에 추가하면된다.
- 이미 실행 중인 경우에도 disown을 사용하면 동일한 효과를 얻을 수 있다.
- 마지막으로 실행 중인 프로세스를 제거하려면 다양한 수준의 강제성과 함께
kill
명령을 사용할 수 있다. - 사실 작업 제어(job control)보다는 터미널 멀티플렉서 를 사용하는 것이 추천된다.
- 터미널 멀티플렉서는 일반적인 사용 사례를 처리할 수 있으며 원격 시스템의 작업도 지원한다.
모던 리눅스 명령어
cd
,ls
,find
등의 명령어는 매일 반복해서 사용하는 명령어다.- 이러한 명령어들은 자주 사용되기 때문에 최대한 효율적인 편이 좋다.
- 이처럼 자주 사용되는 명령 중 일부는 이를 대체할 수 있는 모던 명령어가 있다.
- 그중 일부는 drop-in replacement이고 일부는 기능을 확장한 것이다.
- 단, 엔터프레이즈 레벨의 경우 리눅스 배포판에서 이미 검증된 도구를 사용하는 것이 가장 좋은 방법이다.
- 예시
- exa로 디렉터리 내용 나열하기
- bat로 파일 내용 보기
- rg로 파일에서 콘텐츠 찾기
- jq로 처리하는 JSON 데이터
- 모던 명령어에 대해 더 알고 싶거나 대체할 수 있는 다른 명령어들에 대해 자세히 알고 싶다면 깃허브 modern-unix 저장소를 확인해보자.
일반 작업
자주 사용하는 명령어 단축해보기
- 인터페이스의 기본 개념 중 한 가지는 ‘가장 자주 사용하는 명령은 빠르게 입력해야 하므로 최소한의 노력만 들여야한다’ 이다.
- 예를 들어 이 개념을 셸에 적용하여
git diff --color-moved
를 alias로d
를 지정한다.
행 탐색과 조작
- 셸 프롬프트에 명령을 입력할 때는 행을 탐색하거나, 행을 조작하는 등 다양한 작업을 하곤 한다.
동작 | 명령어 | 비고 |
---|---|---|
행의 시작으로 커서 이동 | Ctrl + a | - |
행의 마지막으로 커서 이동 | Ctrl + e | - |
커서를 한 문자 앞으로 이동 | Ctrl + f | - |
커서를 한 문자 뒤로 이동 | Ctrl + b | - |
커서를 한 단어 앞으로 이동 | Alt + f | - |
커서를 한 단어 뒤로 이동 | Alt + b | - |
현재 문자 삭제 | Ctrl + d | - |
커서 왼쪽 문자 삭제 | Ctrl + h | - |
커서 왼쪽 단어 삭제 | Ctrl + w | - |
커서 오른쪽의 모든 항목 삭제 | Ctrl + k | - |
커서 왼쪽의 모든 항목 삭제 | Ctrl + u | - |
화면 지우기 | Ctrl + l | - |
명령어 취소 | Ctrl + c | - |
실행 취소 | Ctrl + _ | 배시 셸에만 해당 |
기록 검색 | Ctrl + r | 일부 셸만 해당 |
검색 취소 | Ctrl + g | 일부 셸만 해당 |
파일 내용 관리
- 텍스트 한 줄을 추가하기 위해 매번 vi 같은 편집기를 실행하는 것은 비효율적이다.
- 또한 가끔은 편집기 사용이 불가능할 때도 있다.
- 이와 같은 경우 아래와 같은 방법으로 텍스트 내용을 조작할 수 있다.
1
2
3
4
5
6
7
8
9
10
echo "First line" > /tmp/something # echo 출력을 재지정해 파일 생성
echo "Second line" >> /tmp/something # >> 연산자를 사용해 파일에 한 행을 추가한 후 내용을 확인
sed 's/line/LINE/' /tmp/something # sed를 사용해 파일 내용을 바꿈
cat << 'EOF' > /tmp/another # here 문서를 사용해 파일을 생성한다.
diff -y /tmp/something /tmp/another #생성한 파일의 차이점을 보여줌
긴 파일 보기
- 셀의 한 화면에 표시할 수 없을 만큼 행 수가 많은 파일의 경우
less
혹은bat
와 같은 페이저(pager)를 사용할 수 있다. - Paging 기능을 활용하면 프로그램은 출력을 분할해서 화면에 나타낼 수 있는 분량에 맞게 페이지를 나눠 표시하고, 각 페이지를 탐색할 수 있는 명령어를 제공한다.
- 긴 파일을 처리하는 또 다른 방법은
head
,tail
을 활용하여 선택 영역만 표시하는 것이다.
1
2
3
head -5 /tmp/longfile # 긴 파일의 처음 다섯 행을 출력한다.
sudo tail -f /var/log/Xorg.0.log
날짜와 시간 처리
date
명령은 고유한 파일 이름을 생성할 때 유용하다.- 유닉스 타임스탬프 등 여러 형식으로 날짜를 생성하고 다양한 날짜와 시간 형식 간에 변환할 수도 있다.
1
2
3
date +%s # 유닉스 타임스탬프를 생성한다.
date -d @1629742883 '+%m/%d/%Y:%H:%M:%S' # 유닉스 타임 스탬프를 사람이 읽을 수 있는 날짜로 변환한다.
터미널 멀티플렉서
tmux
- tmux는 유연하면서 자료도 풍부한 멀티 플렉서이다.
- 세가지 핵심 요소
- 세션(Session) : 특정 작업을 위한 작업 환경으로 생각할 수 있는 논리 단위이다.
- 그 밖의 모든 단위가 세션에 속한다.
- 윈도(Window) : 세션에 속한 단위며 브라우저의 탭처럼 생각할 수 있다. 사용 여부는 선택 사항이며 대부분 세션당 하나의 윈도만 있다.
- 영역(pane) : 사실상 실행 가능한 단일 셸 인스턴스이다. 영역은 윈도의 일부이며 세로나 가로로 쉽게 분할할 수 있을 뿐 아니라 필요에 따라 확장/축소하고 닫을 수 있다.
- 세션(Session) : 특정 작업을 위한 작업 환경으로 생각할 수 있는 논리 단위이다.
- tmux가 서버로 실행되고 tmux에서 구성한 셸이 클라이언트로 실행되는 구조이다.
- 이 클라이언트/서버 모델을 사용하면 세션을 생성, 시작, 종료, 삭제할 수 있으며 해당 세션에서 실행 중인 셸을 이용할 때 그 안에서 실행(또는 실패)하는 프로세스를 생각할 필요가 없다.
- tmux는 접두사 혹은 트리거 라고 부르는 기본 키보드 단축키로
Ctrl + b
를 사용한다.- 좀 더 쉽게 트리거를 사용하기 위해 단일키로 매핑할 수도 있다.
- 그 외에 각종 shortcut은 공식 문서를 참조 바란다.
스크립팅
- 해당 게시물에서는 배시 셸로 스크립트를 작성하는 데 중점을 둘 것이다.
- 그 이유는 2가지가 있다
- 대부분의 스크립트는 배시 셸로 작성됐으므로 그만큼 배시 셸 스크립트에 사용할 수 있는 예제와 도움말을 찾기 쉽다.
- 대상 시스템에서는 대부분 배시 셸을 사용할 가능성이 높으므로 배시 셸에 대한 대안을 사용하는 경우보다 사용자 풀이 커진다.
- 단 만일 수천 줄의 코드를 기록해야 한다면 차라리 파이썬이나 루비 같은 스트립팅 언어를 사용하는 것이 더나은 선택지일 것이다.
스크립팅의 기본 개요
고급 데이터 유형
- 셸은 일반적으로 모든 것을 문자열로 취급하지만 배열과 같은 일부 고급 데이터 유형은 지원한다.
1
2
3
os=('Linux', 'macOS', 'Windows')
echo "${os[0]}"
numberofos="${#os[@]}"
흐름 제어
- 흐름 제어를 사용하면 스크립트에서 분기(if) 또는 반복(for와 while)을 통해 특정 조건에 따라 실행되게 제어할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
for afile in /tmp/*; do # 디렉터리를 반복하며 각 파일명을 출력하는 기본 루프
echo "$afile"
done
for i in {1..10}; do
echo "$i"
done
while true; do
...
done
함수
- 셸에서 스크립트를 위에서부터 아래로 해석하므로 함수는 미리 정의한 뒤에 사용해야 한다.
1
2
3
4
5
6
sayhi(){ # 함수 정의. 매개변수는 $n를 통해 암시적으로 전달된다.
echo "Hi $1 hope you are well!"
}
sayhi "Michael"
고급 I/O
read
를 사용하면 런타임 입력을 유도할 때 사용할 수 있는stdin
에서 사용자 입력을 읽을 수 있다.- 추가적으로,
echo
를 사용하는 대신 색상을 포함해 출력을 세밀하게 제어할 수 있는printf
를 고려해보는 편이 좋다.printf
는echo
보다 이식성도 뛰어나다.
1
2
read name # 사용자 입력에서 값을 읽는다.
printf "Hello %s" "$name" # 이전 단계에서 읽은 값을 출력한다.
- 시그널이나 트랩 같은 여러 고급 개념도 사용할 수 있다.
- 이곳에서는 스크립팅 주제에 대한 개요만 소개하기에 생략한다.
- 전체적인 자료는 bash scripting cheatsheet를 참고바람.
- bash cookbook이라는 책도 좋은 시작점이다.
이식 가능한 배시 스크립트 작성
- 이식 가능하다 는 말이란 스크립트가 실행될 환경에 대해 암시적이든 명시적이든 너무 많은 가정을 하지 않는다는 의미이다.
- 이식 가능한 스크립트는 다양한 시스템에서 실행할 수 있다.
- 그러나 셸의 유형을 하나로 정하더라도 버전이 다르면 모든 기능이 동일한 방식으로 동작하지 않을 수 있는 점을 기억하자.
- 결국에는 얼마나 다양한 환경에서 스크립트를 테스트할 수 있는지가 중요하다.
이식 가능한 스크립트 실행
- 스크립트는 단순한 텍스트 파일이다.
- 즉 확장자는 중요하지 않지만
.sh
가 관례로 사용되는 경우가 많다. - 이런 텍스트 파일을 실행 가능한 스크립트로 바꾸며 셸에서 실행할 수 있는 것은 다음 두가지 사항 덕분이다.
- 텍스트 파일은 첫 번째 행에서
#!
로 시작하는 셔뱅(shebang또는 hashbang)을 사용해 인터프리터를 선언해야 한다. - 그런 다음
chmod +x
등을 사용해 스크립트를 실행 가능하게 만들어야 한다.- 단, 아무나 실행할 수 없도록 스크립트와 연관된 사용자와 그룹만 스크립트를 실행할 수 있게 허용하는
chmod 750
으로 하는편이 좋다.
- 단, 아무나 실행할 수 없도록 스크립트와 연관된 사용자와 그룹만 스크립트를 실행할 수 있게 허용하는
- 텍스트 파일은 첫 번째 행에서
스켈레톤 템플릿
- 시작점으로 사용할 수 있는 이식가능한 배시 셸 스크립트의 스켈레톤 템플릿은 아래와 같다.
1
2
3
4
5
6
7
8
#!/user/bin/env bash
set -o errexit # 오류가 발생하면 스크립트 실행을 중지한다는 정의
set -o nounset # 설정되지 않은 변수를 오류로 처리하지 않도록 정의(그래야 스크립트가 아무 에러 신호 없이 실패할 가능성이 적다)
set -o pipefail # 파이프의 한부분이 고장나면 전체 파이프가 고장난 것으로 간주하도록 정의한다.
firstargument="${1:-somedefaultvalue}" # 기본값이 있는 명령행 매개변수의 예제
echo "$firstargument"
우수 사례
- 개인적인 용도로 작성하는 스크립트와 수천 명의 사용자에게 제공하는 스크립트 사이에는 차이가 있지만 일반적으로 스크립트를 작성하는 높은 수준의 우수 사례는 아래와 같다.
- 빠르고 요란하게 실패해야 한다 :조용하게 실패하는 것을 피하고 빠르게 실패한다. 이는 errexit와 pipefail 등을 이용하면 가능하다. 배시는 기본적으로 조용히 실패하는 경향이 있으니 아무도 모르게 실패해서는 안되며, 또한 빠르게 실패해야 한다.
- 민감한 정보 : 암호와 같은 민감한 정보를 스크립트에 하드코딩해서는 안된다. 이런 정보는 사용자 입력이나 API 호출을 통해 런타임으로 제공돼야 한다. 또한
ps
는 프로그램 매개변수 등을 공개하며, 이를 통해 민감한 정보가 유출될 수 있다는 점도 고려해둬야 한다. - 입력값 처리 : 가능하다면 변수에 정상적인 기본값 설정, 제공하고 사용자나 기타 소스로부터 받은 입력을 깔끔하게 정리해야 한다. 예를 들어 변수가 설정되지 않았다는 이유로 얼핏 문제 없어 보이는
rm -rf "$PROJECT/"*
가 드라이브를 지우는 상황을 피하기 위해서는 제공된 매개변수를 사용하거나read
명령을 통해 대화식으로 수집한 값을 사용하면 된다. - 의존성 확인 : 내장된 도구이거나 대상 환경을 알고 있지 않는 한, 특정 도구나 명령을 사용할 수 있다고 가정하지 말아야 한다. 자신의 컴퓨터에
curl
이 설치돼 있다고 해서 대상 컴퓨터에도curl
이 반드시 설치돼 있다는 보장은 없다.- 예를 들어
curl
을 사용할 수 없으면wget
을 사용하는 등 말이다.
- 예를 들어
- 에러 처리 : 스크립트가 실패했을 때 사용자가 실행할 수 있는 지침을 제공해야 한다. 예를 들어
Error 123
같은 메시지를 표시하기 보다는, 실패한 항목과 사용자가 상황을 해결할 수 있는 방법을 알려줘야한다 - 문서화 : 메인 블록의 스크립트를 인라인으로 문서화하고, 가독성과 비교를 위해 80열 너비를 지키는 것이 좋다.
- 버전 관리 : 깃을 사용한 스크립트 버전 관리를 고려해야 한다.
- 테스트 : 스크립트를 린트(lint)하고 테스트한다.(이는 매우 중요한 관행이므로 다음 절에서 더 자세히 살펴본다)
스크립트 린트와 테스트
- 개발하는 동안 명령과 지침을 올바르게 사용하고 있는지 점검하기 위해 스크립트를 확인하고 린트해야 한다.
- ShellCheck를 로컬 컴퓨터에 설치하거나, shellcheck.net에서 온라인에서 활용하는 것이 좋다.
- 또한 스크립트의 포맷을 shfmt로 만드는 것도 좋다.
- 이는 ShellCheck에서 보고할 수 있는 문제를 자동으로 수정한다.
- 또한 스크립트는 저장소에 check-in하기에 앞서
bats
를 사용해 테스트하는 편이 좋다. bats
를 사용하면 테스트 케이스를 지정할 수 있는 특수 구문과 함께 배시 스크립트 형태로 테스트 파일을 작성할 수 있다.- 각 테스트 케이스는 설명이 붙은 단순한 배시 함수로서, 일반적으로 이런 스크립트를 CI 파이프라인의 일부로 호출한다.
배경
- 유용한 예제를 개발하면서 스크립팅을 배우기 위해 다음과 같은 상황을 가정한다.
- 깃허브 사용자명(github handle)이 주어지면 해당 사용자의 가입 시기와 전체 이름을 써서 다음과 같이 하나의 문장으로 화면에 표시하는 작업을 자동화 하려한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env bash
set -o errexit
set -o errtrace
set -o nounset
set -o pipefail
### 커멘트라인 매개변수 :
targetuser="${1:-mhausenblas}"
### 의존성이 충족되는지 확인:
if ! [ -x "$(command -v jq)" ]
then
echo "jq is not installed" >&2
exit 1
fi
### 주 내용:
githubapi="https://api.github.com/users/"
tmpuserdump="/tmp/ghuserdump_$targetuser.json"
result=$(curl -s $githubapi$targetuser)
echo $result > $tmpuserdump
name=$(jq .name $tmpuserdump -r)
created_at=$(jq .created_at $tmpuserdump -r)
joinyear=$(echo $created_at | cut -f1 -d"-")
echo $name joined GitHub in $joinyear
- 해당 코드에 더해 모듈성 향상을 위해 bashing, rerun같은 프레임워크를 사용해보는 것도 고려할만하다.