home search
OS
Linux
[Linux] Bash (2): bash shell
2022. 02. 27

shell

개요

shell(셸, 쉘)은어플리케이션을 실행시키는 도구로 또다른 어플리케이션이다. 우리는 OS 커널에 직접 접근할 수 없기 때문에 명령어 해석기인 셸이 필요하다. 셸에서 내리는 명령어는 해석된 뒤에 자식 프로세스로 만들어져 실행되며, 실행된 프로세스는 시스템(커널)이 관리한다. 커널은 셸이 넘겨준 명령어 프로세스를 관리하는 역할을 한다.

특수문자

아래는 자주 사용되는 특수문자와 그 명칭이다.

보통 ^는 ctrl키, ~는 shift키, M은 alt 키를 의미한다. M-C하면 Alt-C라는 것이다.

prompt

prompt(프롬프트)는 input mode(입력기)이다. 시스템 전체로 본다면 터미널 창에 $, # 뒤에 치는 모든 것이 하나의 큰 텍스트 파일인 셈이다. 한 줄을 치고 엔터 치는 동시에 그것을 읽어 실행하고 다음 줄을 보여주는 것이다. bash는 2가지 방식의 입력기 지원하는데 하나가 vi, 다른 하나가 emacs이다. 입력기 모드는 exclusive option으로 작동하여 어느 하가나 쓰이고 있으면 다른 하나는 쓰이지 못한다. 디폴트는 emacs로, bash 기본 모드이다. vi는 ksh와의 호환을 위해 쓰인다.

현재 설정된 옵션을 보기 위해서는 아래와 같은 명령을 쓸 수 있다.

🔥 set -o: human readable한 형태로 모든 옵션을 보여줌. on/off로 표시됨

🔥 set +o: machine readable한 형태로 모든 옵션을 보여줌. -(on) / +(off)로 표시됨

🔥 shopt -p: 모든 옵션을 보여줌

옵션 중 emacs가 on이면 vi가 off로 되어 있을 것이다.

특정 옵션을 켜고 끄기 위해서는 아래처럼 한다. -(on) / +(off)이다.

🔥 set <-/+>o <옵션 이름>

$ set -o emacs
$ set -o vi

recall cmd line args

이전 명령어의 argumet를 다시 불러올 수 있는 키들로 매우 유용하게 쓰인다.

  • Alt-. : 이전 명령어의 마지막 인자를 카피해온다. 만약 이전 명령어가 ls /mydir/src라고 하면 다음 줄에서 cd <Alt-.>을 하면 /mydir/src가 입력된다.
  • !:n-m: 이전 명령어의 n~m번째 argument list를 가져온다. range 지정 시 사용한다. 만약 ‘abc ef gh 123’이 있었다고 했을 때 !:2이면 gh가 오는 것이다.
  • ![i]:n: i번째 히스토리 명령어의 n번째 argument이다. i 생략시 이전 명령을 의미한다. n이 $으로 하면 마지막 인자를 뜻한다. 이 경우 :은 생략 가능하다. (!$, $_) n이 ^인 경우는 첫 번째 인자를 말한다. $0로 0번째 인자는 명령어이다.
$ echo "Hello Unix"
$ find / -name "lib*"
$ echo "Gnu is Not Unix"
$ ps -eo 'pid,cp,cmd'
$ echo "Hello POSIX"
$ find ~ -type f
$ ps aux

^R을 누르면 search 한다는 메시지가 뜰 것이다. U만 치면 세 번째 명령어가 튀어나올 것이다. (그 단어가 나온 걸 찾아줌). 한 번 더 ^R하면 U가 나오는 다음 명령을 찾아주므로 첫 번째 명령어가 나온다. ^R누르고 fi를 치면 아래서 두번째 명령어가, 한 번 더 하면 위에서 두번째 명령어가 나온다. 참고로 명령어 취소는 ^C이므로 선택을 취소할 때 누르고, 걸린 명령을 실행하려면 엔터키를 누른다. 그 명령 가져와 다른 내용을 입력하고 싶으면 탭 키로 카피 가능하다.


bash shell 작동 원리

sub-shell

셸에서 실행되는 유닉스 명령어는 sub-shell이 생성되어 실행된다. 이때 서브 셸은 fork 된 뒤 exec(실행)되는 순서(fork-exec)로 만들어지는 child process(자식 프로세스) 셸을 말한다. 외부 명령어 실행은 기본적으로 서브 셸을 생성하므로, 프로세스 생성의 오버헤드가 존재한다.

ls -al명령을 실행하면 bash(부모 프로세스)는 C 언어 함수인 fork를 통해 서브쉘을 만들어 이것이 자식 프로세스가 된다. 자식 프로세스는 부모를 복사해오므로 bash 쉘을 똑같이 가진다. 자식은 ls를 실행해야 하므로 이미지를 교체하는 명령인 C 언어 함수 exec를 내린다. 실행이 종료된다면 종료되었다는 exit code를 부모 프로세스로 반환한다.(return code) 이는 $? 변수로 확인 가능하다.

exit code

부모 프로세스는 자식 프로세스의 exit code를 통해 자식 프로세스의 성공/실패 여부를 알고, 성공했다면 다음 작업을, 실패했다면 에러 처리(정지)를 한다. 그래서 종료 코드를 받는 것이 중요하다. exit code는 0은 true(성공)을, 그 외 non-zero는 false(실패)를 뜻한다. 바쉬 셸의 exit code는 C언어의 exit 함수와도 관련이 깊으며, C언어의 main 함수 마지막에 return 0도 성공했음을 리턴하라는 의미이다.

서브 셸의 exit code를 더 자세히 보자. 서브 쉘에세 일을 분담하는 것은 마치 하나의 함수를 호출하는 행위와 비슷하다.

A를 B로 복사한다. →→→→ cp A B로 구현 가능
if(복사가 실패했는가?)
{
	에러 처리 후 종료
}

자식 프로세스를 만들어 exit 코드를 받고, 성공 실패에 따라 분기하는 것은 multi-process 의 처리 방식이다.

exit code를 직접 살펴보자. 우선 ‘Hello world’라는 C언어로 된 파일을 만들어본다.

#include <stdio.h>
#include <unistd.h>
int main(){
	dprintf(STDOUT_FILENO, "Hello world\n");
    return 234;
}

파일의 내용을 확인하면(cat HelloWorld.c) 해당 내용을 그대로 담고 있음을 볼 수 있다. 다음으로 파일을 make해보자.

$ make helloworld
cc	hworld.c	-o hworld

해당 명령은 자동으로 컴파일러(cc)를 불러와서 컴파일한다. gcc helloworld.c은 옛날 방식이다.

이제 실행을 하고 exit code를 받아본다.

$ ./helloworld
Hello world

$ echo $?
234

서브 쉘(자식프로세스)의 리턴값, 즉 main 함수의 리턴 값인 234가 잘 나왔다. 참고로 main의 리턴값은 8비트이다. 따라서 main의 리턴형인 int가 32비트형이라 해도 이를 다 쓰지 못한다. 리턴 값으로 2340은 오버플로우가 발생한다.

이번에는 스크립트 리턴 값이 아닌 일반 명령어의 리턴값을 보자. 셸에서 실행되는 모든 자식 프로세스는 리턴값을 가진다. 앞서 본 바와 같이 0은 sucess, 나머지 양수(non-zero)는 fail이다.

$ touch qwer
$ rm qwer
$ echo $?
0

$ rm qwer
$ echo $?
1

처음 qwer이란 파일을 만들고 이를 지웠을 때, rm 명령어는 명령을 잘 수행했으므로 성공인 0을 반환했다. 반면 rm을 다시 실행하면 지울 파일이 없으므로 실패다. 따라서 1을 반환했다. 참고로, bash 실행 시 errexit 기능이 켜져있는 경우라면 exit code가 non-zero 시 자동으로 종료된다.

(참고) rm –force

UNIX command, 특히 rm과 같은 optional한 명령에 --force 옵션이 존재하는 건, 자식 프로세스로 작동할 때 exit code를 조작하기 위해 존재하는 기능들이다. -f옵션은 리턴 값을 무시할 때 주로 사용한다. rm --force 하면 파일이 있든 없든, 지웠든 안 지웠든, 리턴값이 0이든 1이든 그냥 하고 넘어간단 의미이다.

$ rm -f qwer 
$ echo $?
0

위 코드에서 qwer 파일이 없어도 종료 코드는 0이 나온다. -f 옵션을 주었기 때문에 에러코드가 변경된다는 것이다.

rmforce 옵션은 Makefile에서도 사용되는 기능이다. rm 명령어의 결과를 무시해야 하는 경우에 -rm으로 많이 사용한다.

Makefile을 하나 만들어보고 실습해보자.

all :
	@echo 'Clear temporary files'
    rm *.tmp
    @echo 'All temp files are deleted on current dir.'

파일 내용을 확인하고, 임시 파일인 ‘hello.tmp’를 만들어본 뒤, make 실행해보자.

$ cat MakeFile
all :
	@echo 'Clear temporary files'
    rm *.tmp
    @echo 'All temp files are deleted on current dir.'
    
$ touch hello.tmp
$ ls
Makefile	hello.tmp

$ make
Clear temporary files
rm *.tmp
All temp files are deleted on current dir.

첫 실행은 잘 된다. 지울 파일이 존재했기 때문이다. 다시 시도해보자.

$ ls
Makefile

$ make
Clear temporary files
rm *.tmp
rm: cannot remove '*.tmp': No such file or directory
maek: `* [Makefile:3: all] Error 1

에러가 났다. echo $?로 찍어보면 exit code가 1로 나올 것이다. rm이 에러가 나온 것으로, rm *.tmp에서 멈췄다. 지울 파일이 없어도, 이 뒤에 어떠한 작업 해야할 경우, 여기서 멈추면 곤란하다. 따라서 이런 경우 force 옵션을 사용한다. MakeFile의 rm 명령을 아래와 같이 바꾼다.

all :
	@echo 'Clear temporary files'
    rm -f *.tmp
    @echo 'All temp files are deleted on current dir.'

이렇게 하면 에러가 생겨도 무조건 0으로 리턴한다.

$ ls
Makefile

$ make
Clear temporary files
rm *.tmp
All temp files are deleted on current dir.

$ echo $?
0

즉, force는 강제한다는 뜻이 아닌 exit code(ec 또는 rc(return code))를 무시한다는 뜻이다. make외에도 gradle이나 다른 빌드 툴도 동일한 기능으로 많이들 작동한다.

zombie

부모 프로세스가 종료된 자식 프로세스의 exit status를 처리하기 전의 상태에서, 그 자식 프로세스를 ‘defunct 프로세스’ 혹은 ‘좀비 프로세스’라고 부른다.

프로세스나 파일 등은 전부 ‘데이터’ + ‘메타 정보’로 구성되어 있다. 데이터는 그 파일의 내용을 말하고, 메타 정보에는 pid, 시간, 파일 이름 등 해당 파일 자체에 대한 정보가 있다. exit status 역시 메타 정보에 있다. 프로세스가 종료되면 메타 정보만 남고 데이터 영역과 자원은 운영체제가 회수해간다. 이렇게 메타 정보만 살아있어 ‘좀비 프로세스’라 불렀던 것이다. 실상 이들은 메모리 차지는 많이 안한다.

모든 프로세스의 종료는 좀비 상태를 거쳐서 해소되므로, 어감은 별로지만 좀비 프로세스는 프로세스가 종료되는 정상적 과정 중 하나이다.

shell script 작성 시 유의점

UNIX 계열의 시스템 프로세스에서 return code는 프로세스의 main 함수의 리턴값(exit code)이며, 해당 리턴 값은 부모 프로세스에게 보고된다. 시그널을 수신해 종료된 경우라면 시그널 번호를 전한다. 이로써 부모 프로세스는 자식 프로세스가 어떻게 종료되었는지 알 수 있다.

이를 적절히 활용하기 위헤 shell 스크립트를 쓸 때도 명령어들이 exit code나 시그널 핸들링으로 작성되어야 한다. 정상 작동이 아닐 경우 return 코드를 non-zero로 넘겨주도록 말이다.


invoking type

invoking mode

셸을 Authentication 과정에 따라 login shell / non-login shell로, command-execution 방식에 따라 interactive shell / non-interactive shell로 나뉜다. 둘의 조합으로 3가지 실행방식이 조합되어 사용되는데, 아래와 같다.

  • interactive login shell
  • interactive non-login shell
  • non-interactive shell : 항상 non-login shell로 작동하며, shell script를 실행.

login / none-login shell

① login shell 로그인 할 때 실행되는 셸의 타입이다.

해당 셸은 /etc/profile 파일이 존재한다면 이것을 import 해온다. 그 다음으로는 특정 사용자에 대한 profile을 읽는다. ~/.bash_profile~/.bash_login~/.profile 순으로 앞의 것이 없다면 그 다음 것으로 읽어들인다. RH 계열은 .bash_profile을, Debian 계열은 .profile을 주로 사용한다. 종료할 때는 ~/.bash_logout이 존재한다면 이를 실행한다.

② non-login shell X window에서 터미널을 실행했을 때가 그 예시이다. su로 열리는 셸도 그렇다. 단 su -로 열리는 경우는 login_shell로 열린다. 작동 시에는 ~/.bash_rc가 존재할 경우 이를 먼저 import 해온다.

사실 login_shell이 non-login shell보다 더 큰 범위이다. ~/.bash_profile에서 ~/.bash_rc가 import되어 실행된다. ~/.bash_profile에는 아래와 같은 부분이 있다. ~/.bashrc가 있다면 이를 import 하라는 뜻이다.

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

필자는 Debian계열의 우분투를 사용하므로 .profile을 한 번 편집해보겠다. 맨 아래에 다음과 같은 내용을 추가한다. xterm의 색상을 추가하는 내용이다.

export TERM=xterm-256color

alias를 지정하려면 Debian 계열은 ~/.bash_aliases를 편집하고, RH 계열은 ~/.bashrc를 직접 편집하거나 ~/.bashrc에서 ~/.bash_aliases를 import한다.

이 역시 실습해본다. 먼저 ~/.bash_rc을 열어 아래 내용을 추가해 ~/.bash_aliases를 가져온다.

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

이번엔 ~/.bash_aliases를 열어 아래 내용을 추가한다. 순서대로 enus라고 명령하면 언어를 영어로 한다는 뜻이고, kokr로 명령하면 한국어로 한다는 뜻, vi로 하면 vim 편집기를 연다는 뜻, man하면 영어로 man page를 연다는 뜻이다.

alias enus="export LANG=en_US.UTF-8; export LANGUAGE="
alias kokr="export LANG=ko_KR.UTF-8; export LANGUAGE=en:ko"
alias vi=vim
alias man="LANG=en_US.utf8 man"
$ cat ~/.bash_aliases
alias enus="export LANG=en_US.UTF-8; export LANGUAGE="
alias kokr="export LANG=ko_KR.UTF-8; export LANGUAGE=en:ko"
alias vi=vim
alias man="LANG=en_US.utf8 man"

import, source 명령

환경 변수나 셸 스크립트를 현재 셸에서 실행한다. 즉, 그저 읽어온다. .(bourne shell 명령어)이나 source(bash 명령어)을 사용한다. ~/.bashrc~/.bash_profile에서 다른 설정을 import하거나, 셸 스크립트 코드에서 script library 모듈을 import하는 경우에 이런 식으로 사용한다.

아래 명령은 ./bashrc를 다시 읽어오는 예이다.

$ . ~/.bashrc

interactive / non-interactive mode

interactive 모드는 한 명령을 치면 그 결과를 보여주는 식으로 작동하고, non-interactive 모드는 sub-shell을 생성해 스크립트를 처리하는 식으로 작동한다.

먼저 아래와 같은 파일을 만들어둔다. cat 명령으로 확인한다.

$ cat hellobash.sh → 아래 내용을 입력해 만든다.

#!/bin/bash
echo Hello bash
echo $-

실행 권한을 부여한다.

$ chmod 755 ./hellobash.sh

먼저, non-interactive mode의 경우 아래와 같이 실행하고 그 결과를 볼 수 있다

$ ./hellobash.sh

Hello bash
hB

h, B만 켜진 것을 볼 수 있다

다음으로 interactive mode의 경우를 보자.

$ source ./hellobash.sh
Hello bash
himBH

h부터 H까지 다섯 개가 다 켜진 것을 볼 수 있다. ‘himBH’ 같은 것은 bash special parameters이다.

non-interactive mode에서 옵션을 변경하는 방법은 두 가지가 있다.

🔥 bash <-옵션명> <파일이름>

🔥 set -o 옵션명: 셸 스크립트 내에서

아래는 첫 번째 방식을 적용한 예이며, e(errexit, -e)는 에러 발생 시 exit하라는 옵션이다.

$ bash -e ./hellobash.sh

Hello bash
ehB			# 옵션 e가 추가됨

이번엔 스크립트에서 추가해보자.

$ cat hellobash.sh

#!/bin/bash
set -o errexit		# 추가
echo Hello bash
echo $-

$ chmod 755 ./hellobash.sh

$ ./hellobash.sh

Hello bash
ehB		# 옵션 e가 추가됨
arrow_upward arrow_downward
loading