Home 모던 리눅스 교과서 3. 셸과 스크립팅
Post
Cancel

모던 리눅스 교과서 3. 셸과 스크립팅

  • 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) : 사실상 실행 가능한 단일 셸 인스턴스이다. 영역은 윈도의 일부이며 세로나 가로로 쉽게 분할할 수 있을 뿐 아니라 필요에 따라 확장/축소하고 닫을 수 있다.
  • 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를 고려해보는 편이 좋다. printfecho보다 이식성도 뛰어나다.
1
2
read name # 사용자 입력에서 값을 읽는다.
printf "Hello %s" "$name" # 이전 단계에서 읽은 값을 출력한다.
  • 시그널이나 트랩 같은 여러 고급 개념도 사용할 수 있다.
  • 이곳에서는 스크립팅 주제에 대한 개요만 소개하기에 생략한다.

이식 가능한 배시 스크립트 작성

  • 이식 가능하다 는 말이란 스크립트가 실행될 환경에 대해 암시적이든 명시적이든 너무 많은 가정을 하지 않는다는 의미이다.
  • 이식 가능한 스크립트는 다양한 시스템에서 실행할 수 있다.
    • 그러나 셸의 유형을 하나로 정하더라도 버전이 다르면 모든 기능이 동일한 방식으로 동작하지 않을 수 있는 점을 기억하자.
    • 결국에는 얼마나 다양한 환경에서 스크립트를 테스트할 수 있는지가 중요하다.

이식 가능한 스크립트 실행

  • 스크립트는 단순한 텍스트 파일이다.
  • 즉 확장자는 중요하지 않지만 .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"

우수 사례

  • 개인적인 용도로 작성하는 스크립트와 수천 명의 사용자에게 제공하는 스크립트 사이에는 차이가 있지만 일반적으로 스크립트를 작성하는 높은 수준의 우수 사례는 아래와 같다.
  1. 빠르고 요란하게 실패해야 한다 :조용하게 실패하는 것을 피하고 빠르게 실패한다. 이는 errexitpipefail 등을 이용하면 가능하다. 배시는 기본적으로 조용히 실패하는 경향이 있으니 아무도 모르게 실패해서는 안되며, 또한 빠르게 실패해야 한다.
  2. 민감한 정보 : 암호와 같은 민감한 정보를 스크립트에 하드코딩해서는 안된다. 이런 정보는 사용자 입력이나 API 호출을 통해 런타임으로 제공돼야 한다. 또한 ps는 프로그램 매개변수 등을 공개하며, 이를 통해 민감한 정보가 유출될 수 있다는 점도 고려해둬야 한다.
  3. 입력값 처리 : 가능하다면 변수에 정상적인 기본값 설정, 제공하고 사용자나 기타 소스로부터 받은 입력을 깔끔하게 정리해야 한다. 예를 들어 변수가 설정되지 않았다는 이유로 얼핏 문제 없어 보이는 rm -rf "$PROJECT/"*가 드라이브를 지우는 상황을 피하기 위해서는 제공된 매개변수를 사용하거나 read 명령을 통해 대화식으로 수집한 값을 사용하면 된다.
  4. 의존성 확인 : 내장된 도구이거나 대상 환경을 알고 있지 않는 한, 특정 도구나 명령을 사용할 수 있다고 가정하지 말아야 한다. 자신의 컴퓨터에 curl이 설치돼 있다고 해서 대상 컴퓨터에도 curl이 반드시 설치돼 있다는 보장은 없다.
    • 예를 들어 curl을 사용할 수 없으면 wget을 사용하는 등 말이다.
  5. 에러 처리 : 스크립트가 실패했을 때 사용자가 실행할 수 있는 지침을 제공해야 한다. 예를 들어 Error 123같은 메시지를 표시하기 보다는, 실패한 항목과 사용자가 상황을 해결할 수 있는 방법을 알려줘야한다
  6. 문서화 : 메인 블록의 스크립트를 인라인으로 문서화하고, 가독성과 비교를 위해 80열 너비를 지키는 것이 좋다.
  7. 버전 관리 : 깃을 사용한 스크립트 버전 관리를 고려해야 한다.
  8. 테스트 : 스크립트를 린트(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같은 프레임워크를 사용해보는 것도 고려할만하다.
This post is licensed under CC BY 4.0 by the author.

모던 리눅스 교과서 2. 리눅스 커널

모던 리눅스 교과서 4. 접근 제어