업데이트(2020.12.26): 변수 관련 parameter expansion과 따옴표 관련 내용 추가

업데이트(2019.09.08): 배열과 조건문에 예제들 추가

쉘 스크립트 사용법을 간단하게 정리하고 예제를 사용해보자


환경

  • Linux 기반 시스템
  • Bash shell(/bin/bash)


쉘 스크립트 작성 및 권한 부여

파일생성 및 권한 부여

  • 파일생성
$ touch shell_script_practice.sh // 파일 생성
$ vim shell_script_practice.sh // 쉘 스크립트 파일 편집기로 열기
  • 실행 권한 부여(파일의 상태 변경)
$ chmod +x shell_script_practice.sh // 실행 권한 부여


스크립트 상단에 #!/bin/bash 추가

#!/bin/bash

... 하단에 스크립트 작성 ...


쉘 스크립트 실행 방법

  • ./[쉘스크립트 파일명]
$ ./shell_script_practice.sh


기본 문법과 예제

기본 출력

  • echo, printf
echo "Echo Test" # 자동 개행
printf "printf Test" # 자동 개행X
printf "%s %s" print test # 뒤에 오는 문자열들이 전달되는 인자라고 생각하면 됩니다.
  • $#: 스크립트에 전달되는 인자들의 수(C언어에서 argc)
  • $0: 실행하는 스크립트의 파일명으로 실행했을 때 경로를 포함한다면 경로를 포함해서 나옵니다.
  • $1, $2 …: 스크립트로 전달된 인자들(C언어에서 argv[0], argv[1]…)
#!/bin/bash

echo "Echo Test"
printf "printf Test\n"
printf "Name of script: %s\n" $0
printf "%d arguments %s %s\n" $# $1 $2


주석

  • #를 사용
# echo "Echo Test"


변수 선언

  • =를 이용해서 선언하고 $를 이용해서 사용
  • =는 공백 없이 붙여써야한다.
  • 지역변수에는 local을 붙인다.
  • {}parameter expansion으로 $와 함께 감싼 부분에 변수를 대입해준다.(https://superuser.com/questions/935374/difference-between-and-in-shell-script) 여러 표현 방법을 통해 다양하게 사용이 가능하다.
  • 변수가 선언되지 않았을때 default_value=${default_value:="example default value"}처럼 기본값을 사용하도록 설정 가능하다.
  • ""로 감싸서 사용하면 더 안전하다. 문자열에 공백도 포함해서 값을 이용할 수 있기 때문이다. Ex) $ex -> "${ex}"
#!/bin/bash

# shell script variable
test="abc"
num=100

# variable usage
echo ${test}
echo ${num}
echo "${test}"
echo "${num}"

# local variable
local local_val="local one"

# If variable default_value is not set, set it to "example default value" and assign again.
default_value=${default_value:="example default value"}
  • 만약 {}(parameter expansion)을 사용하지 않으면 아래와 같은 경우에 변수 test가 아닌 변수 test5678을 불러오게 된다.
test=1234
echo "This is $test5678" # "This is "

echo "This is ${test}5678" # "This is 12345678"
  • 만약 ""로 감싸지 않으면 아래와 같은 조건문에서 unary operator expected라는 오류가 생길 수 있다.
  • 치환을 하는 형태이기 때문에 아래의 식이 if [ == " " ]로 바뀌기 때문이다.
blank=" "
if [ ${blank} == " " ]
then
        echo "blank test"
fi


배열

기본

  • 아래처럼 배열이름=(원소1 원소2 ...)의 형태로 선언
  • 배열의 인덱스는 0부터 시작합니다.
  • 배열이름[@]는 배열의 모든 원소를 의미합니다.
#!/bin/bash

arr_test_string=("abc" "def" "ghi" "jkl")
echo "${arr_test_string[2]}"

arr_test_char=('a' 'b' 'b')
echo "${arr_test_char[0]}"

arr_test_num=(1 2 3 100 10000)
echo "${arr_test_num[3]}"

echo "${arr_test_num[@]}"

배열에 원소 추가

  • += 연산자를 사용
#!/bin/bash

arr_test_string=("abc" "def" "ghi" "jkl")

arr_test_string+=("mno")
arr_test_string+=("pqr" "stu")

for i in ${arr_test_string[@]}; do
	echo $i
done

arr_test_string=(1 2 3 4 5)

arr_test_string+=(6)
arr_test_string+=(7 8)

for i in ${arr_test_string[@]}; do
	echo $i
done

배열에서 원소 삭제

  • /를 사용해 해당 문자열 부분이 있으면 삭제, 삭제하고자 하는 문자나 문자열이 포함되어있는 부분을 모두 삭제합니다.
  • (권고) unset을 이용해 삭제
#!/bin/bash

arr_test=(1 2 3)
remove_element=(3)

arr_test=( "${arr_test[@]/$remove_element}" )

for i in ${arr_test[@]}; do
	echo $i
done


arr_test=("abc" "def" "ghi")
remove_element=("ghi")

arr_test=( "${arr_test[@]/$remove_element}" )

for i in ${arr_test[@]}; do
	echo $i
done

# !!! Be careful when you delete like below !!!
# Use second method in this case

arr_test=("abc" "def" "defghi")
remove_element=("def")

arr_test=( "${arr_test[@]/$remove_element}" )

for i in ${arr_test[@]}; do
	echo $i
done
#!/bin/bash

arr_test=("abc" "def" "defghi")
remove_element=("def")

# Get index of array
echo ${!arr_test[@]}

for i in ${!arr_test[@]}; do
	if [ ${arr_test[i]} = ${remove_element} ]; then
		# Use unset
		unset arr_test[i]
	fi
done

for i in ${arr_test[@]}; do
	echo $i
done


조건문

#!/bin/bash

# Numeric if statement
test_num=5

if [ "${test_num}" -eq 2 ]; then
	echo "number is 2"
elif [ "${test_num}" -eq 3 ]; then
	echo "number is 3"
else
	echo "number is not 2 or 3"
fi

# Numeric if statement
test_num=5

if (( ${test_num} > 3 )); then
	echo "number is greater than 3"
else
	echo "number is not greater than 3"
fi

# String if statement
test_str="test"

if [ "${test_str}" = "test" ]; then
	echo "test_str is test"
else
	echo "test_str is not test"
fi


반복문

  • while문의 사용
#!/bin/bash

cnt=0
while (( "${cnt}" < 5 )); do
    echo "${cnt}"
    (( cnt = "${cnt}" + 1 )) # 숫자와 변수의 연산은 (())가 필요합니다.
done
  • for문의 사용법
#!/bin/bash

arr_num=(1 2 3 4 5 6 7)

# 배열에 @는 모든 원소를 뜻합니다.
for i in ${arr_num[@]}; do
    printf $i
done
echo

for (( i = 0; i < 10; i++)); do
    printf $i
done
echo


참고자료

Bubble Sort를 구현해보자


환경

  • Python
  • C++


Bubble Sort란?

원리

  • 인접한 2개의 값을 비교한후 값을 변경하는 과정을 통해서 정렬을 진행하는 알고리즘이다.
  • 오름차순의 경우라면 인접한 두 수를 비교했을 때 앞의 수가 더 크다면 그 둘의 값을 바꾸는 방식으로 아래의 사진을 보면 이해가 쉽다.

특징

  • 위에 사진처럼 왼쪽에서부터 오른쪽으로 진행하면서 정렬을 진행한다고 했을 때 1번 왼쪽에서 오른쪽까지 갈 때마다 가장 오른쪽에는 가장 큰 값이 위치하게 된다!
  • 코드의 구현은 쉬우나 복잡도가 O(n^2)의 성능을 나타낸다.


복잡도

  • 시간복잡도: O(n^2)
  • 공간복잡도: O(1)(swap을 위한 변수만 필요합니다.)


구현 코드(Python)


def merge_sort_better(A):

    for i in range(len(A) - 2, 0, -1):
        for j in range(0, i + 1):
            if A[j] > A[j + 1]:
                tmp = A[j]
                A[j] = A[j + 1]
                A[j + 1] = tmp

    return A


def merge_sort_normal(A):

    for i in range(0, len(A)):
        for j in range(0, len(A) - 1):
            if A[j] > A[j + 1]:
                tmp = A[j]
                A[j] = A[j + 1]
                A[j + 1] = tmp
    return A

import random
A = [random.randint(0, 100) for _ in range(0, 10)]
print(A)
merge_sort_better(A)
print(A)



구현 코드(C, C++)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void swap(int *a, int *b) {
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void bubble_sort_better(int a[], int len) {

	for (int i = len - 2; i >= 0; --i) {
		for (int j = 0; j <= i; ++j) {
			if (a[j] > a[j + 1]) {
				swap(&a[j], &a[j + 1]);
			}
		}
	}

}

void bubble_sort_normal(int a[], int len) {

	for (int i = 0; i < len; ++i) {
		for (int j = 0; j < len - 1; ++j) {
			if (a[j] > a[j + 1]) {
				swap(&a[j], &a[j + 1]);
			}
		}
	}

}

int main() {

	int a[5];
	int len = sizeof(a) / sizeof(a[0]); // 배열길이

	// 랜덤함수를 위한 srand와 seed
	srand(time(NULL));

	// 벡터 초기화
	for (int i = 0; i < len; i++) {
		// 1 ~ 50까지의 난수 생성
		a[i] = rand() % 50 + 1;
	}

	// 정렬 전 출력
	for (int i = 0; i < len; i++) {
		printf("%d ", a[i]);
	}
	printf("\n");

	// 거품정렬
	bubble_sort_better(a, len);

	// 정렬 후 출력
	for (int i = 0; i < len; i++) {
		printf("%d ", a[i]);
	}
	printf("\n");

	return 0;
}


참고자료

진행하고 있는 장고 프로젝트에 cron 작업을 추가하는 경험을 정리해본다.


환경 및 선수조건

  • Django
  • crontab의 개념
  • (선택) pyenv와 virtualenv를 이용한 가상환경(가상환경에서도 잘 돌아가고 경로 설정을 자동으로 해줍니다.)


crontab 사용 이유

문제

  • 기존 API를 호출하면 페이지를 크롤링을 하고 그에 대한 결과를 응답하는데 시간이 3초 이상걸린다.

해결

  • 크롤링하고자하는 페이지를 주기적으로 크롤링해서 결과를 DB에 저장해두고 API는 DB에 저장된 값들만 가져온다.


설정

pip를 통한 설치

pip install django-crontab


settings.py에 django_crontab를 추가

settings.py

...
INSTALLED_APPS = (
    'django_crontab',
    ...
)
...


app/cron.py의 파일을 생성 및 함수 만들기

  • 해당 장고프로젝트의 app(본인의 경우에는 main이라는 앱명을 지었습니다.)폴더 안에 cron.py 파일을 생성하고 함수를 만듭니다.
  • 사실 함수를 불러올 수 있는 경로이면 상관 없습니다. 즉, 따로 cron.py를 만들지 않고 경로만 잘 잡아주면 됩니다.

app/cron.py

def my_cron_job():
    pass


settings.py에 있는 CRONJOBS에 작업 추가하기

  • 기본 Linux crontab과 형식이 같습니다.(참고)
  • 기본 구조: * * * * * 수행할 명령어
  • 순서대로 분(0 - 59) 시(0 - 23) 일(1 - 31) 월(1 - 12) 요일(0 - 6) (0:일요일, 1:월요일, 2:화요일, …, 6:토요일)
  • 예제: ('*/5 * * * *', 'app.cron.my_cron_job') -> 매 5분마다 app/cron.py에 있는 my_cron_job()을 실행합니다.

settings.py

...
CRONJOBS = [
    ('*/5 * * * *', 'app.cron.my_cron_job')
]
...


적용

등록된 job들을 모두 실행

python manage.py crontab add

등록된 job들을 모두 제거

python manage.py crontab remove

등록된 job들을 모두 보기

  • django-crontab을 통해서 등록한 job들이 보여집니다.
python manage.py crontab show


확인

  • 위에 나와있는 명령어로 확인이 가능하지만 django-crontab의 경우에는 등록을 하면 Linux crontab에 등록이 된다.
  • crontab -e의 명령어를 통해서 확인이 가능하다.


실수

  • django-crontab을 이용하는 경우에 실제 리눅스에 등록된 crontab이 작동하는거기 때문에 파일의 경로에 신경을 써줘야합니다.
  • 예를 들어서, 위에 cron.py에서 파일을 새로 생성했다면 프로젝트의 앱 폴더(app/)가 아닌! 리눅스 유저 기본 디렉토리(~)에 위치합니다!


참고자료

OS를 공부할 때 나오는 개념인 멀티프로그래밍, 멀티프로세싱, 멀티스레딩, 멀티태스킹에 대해 알아보자


환경 및 선수조건

  • OS에 대한 아주 간략한 이해(OS가 무엇이다 정도)
  • 싱글코어 멀티코어
  • 프로그램, 프로세스 그리고 스레드의 개념


싱글코어와 멀티코어

  • Core: CPU안에서 기초적인 연산을 처리하고 담당하는 부분을 말한다. 사람의 뇌로 생각을 하면 대뇌와 비슷하다.
  • Multi Core: 하나의 CPU안에 여러개의 코어를 구현해 놓은 아키텍처를 멀티코어라고 한다.
  • Hyper-Threading: 인텔에서 개발한 기술로 하나의 코어에 논리적으로 두개 이상의 코어처럼 동작하도록 설계한 기술을 의미한다.


프로그램, 프로세스 그리고 스레드

Program

  • Program: 저장소에 존재하는 코드의 뭉치로 실행상태가 아닌 실행하기 전에 저장소에 저장된 코드들이다. 우리가 흔히 실행한다는 게임이나 도구(Word, Excel등등)들이 해당된다.

Process

  • Process: 저장소에 존재하는 프로그램이 컴퓨터가 실행해서 CPU가 처리할 수 있게 메인 메모리에 올라온 상태이다.

Thread

  • Thread: 프로세스 안에 있는 작은 실행단위로 프로세스와 Data영역과 Code영역은 공유하고 스레드 각자의 스택과 레지스터를 갖는다.


멀티프로그래밍, 멀티프로세싱, 멀티스레딩, 멀티태스킹

멀티프로그래밍

  • 초창기 싱글코어 싱글 스레드 컴퓨터에서는 프로그램이 메모리에 하나만 올라가고 해당하는 프로그램만 CPU가 처리를 진행할 수 있었다. 이런 과정에서 프로그램이 파일 입출력을하게 되면 중간에 idle(정지)상태가 되는데 이 때 다른 프로세스의 일부를 실행시키는걸 의미한다.

멀티프로세싱

  • 하나의 컴퓨터에 여러개의 CPU를 장착하고(CPU속에 멀티코어로 생각해도 무방합니다.) 하나 이상의 프로세스들을 동시에 처리하는걸 의미한다.
  • 더 넓은 범위로 해석하면 하나 또은 그 이상의 프로세스를 여러 장치에서 병렬로 처리하는 것을 의미한다.

멀티스레딩

  • 말 그대로 프로세스내에서 하나의 스레드가 아닌 여러개의 스레드를 형성하여 명령어들을 처리하는걸 의미한다.

멀티태스킹

  • 테스크란 OS에서 처리하는 작업단위를 의미하는데 그 여러 작업 단위가 빠르게 처리되어 동시에 처리되는것처럼 보이는 것을 의미하며 Concurrent라는 특징을 생각하면 된다.
  • 여기서 말하는 여러 작업 단위란 처리해야하는 여러 프로세스들의 명령어들을 쪼갠거라 생각하면 된다.


참고자료

Git에서 작업중인 현재 branch에 있는 commit 내역들을 수정해보자


환경 및 선수조건

  • Git


문제 상황 및 목적

  • 새로운 branch를 만들어서 commit을 2번 하였는데 그 2번의 commit에 다 오타가 있어서 수정하고 싶은 상황


방법

  • rebase를 통해서 수정이 가능하다.
  • git rebase -i HEAD~~: HEAD를 포함해서 2번째 부모 커밋 전까지의 내역을 rebase를 통해서 수정하겠다.


HEAD와 인덱싱을 통해 찾기

  • HEAD: git에서 현재 작업중인 곳을 가리키는 포인터
  • HEAD~ or HEAD~1: HEAD로부터 첫번째 부모 커밋. 즉, 현재의 이전 커밋
  • HEAD~~ or HEAD2: HEAD로부터 두번째 부모 커밋. 즉, 현재의 이전 이전 커밋


실제 적용 사례

rebase 명령어 사용

  • 현재 작업내용을 포함해서 이전 커밋까지 수정한는거니까 아래와 같다.
$ git rebase -i HEAD~~


pick을 edit으로 변경

  • 아래처럼 화면이 뜨면 vim의 방식을 이용해서 pick -> edit으로 변경해준다.
  • i를 눌러서 변경하고 ESC를 누른 다음에 :를 누르고 wq를 누르면 저장된다.
...

pick 6a2298f add main.js to staticd folders
pick 7911e2d 코드에 있는 indentation tab(4spaces)로 수정

...
...

edit 6a2298f add main.js to staticd folders
edit 7911e2d 코드에 있는 indentation tab(4spaces)로 수정

...


중간 확인

  • 그러면 아래처럼 화면에 수정할 수 있도록 명령어를 치라는 화면에 쉘에 나와야 한다.
  • git commit --amend로 수정 한번하고 git rebase --continue를 통해서 다음 수정으로 넘어가는 방식
...

You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

...


수정하기

  • 명령어 실행 및 vim 방식을 통해서 메시지 수정
$ git commit --amend
  • 다음 수정으로 넘어가기
$ git rebase --continue


완료

  • 마지막으로 아래 명령어를 수행하면 성공이라고 뜬다. 뒤에 브랜치 명은 사용한 브랜치에 따라서 다르다.
$ git rebase --continue
Successfully rebased and updated refs/heads/feature/merge-all-js-codes-to-js-file.


참고자료