Shell Script를 활용하여서, 간단한 작업을 자동화할 필요가 있었다.
자동화를 시키기 위해 파이썬을 사용할 수 도 있었지만, Java 기반으로 개발을 하고 있기도 하고, 개발팀원 모두가 설치없이 사용할 수 있도록 Shell Script로 쓰기로 결정했다.
이전에 개인 프로젝트에서 자동배포할 때, Shell Script를 사용했는데, 자세히 공부를 하지 못하고 넘어간 것이 걸려서, 요번에 한번 공부한 내용을 작성해보도록 하겠다.
[목차]
- Shell Scirpt란?
- 기본 문법 정리
- 내가 사용한 코드 전문에서 사용된 문법 공부해보기
- 내가 사용한 코드에서의 유의사항 및 개선점에 대해 고민해보기
shell은 운영체제 상에서, 사용자가 입력하는 명령을 읽고 해석하여 대신 실행해주는 프로그램입니다. 운영체제의 커널과, 사용자 사이를 이어주는 역할을 하며, 사용자의 명령어를 해석하고 운영체제가 알아들을 수 있도록 도와주는 명령입니다.
Linux에서 사용하는 shell의 종류는 여러가지가 존재합니다.
bash : GNU운영체제, 리눅스, 맥 등 다양한 운영체제에서 사용중이며, 현재 리눅스의 표준 쉘이다.
sh Bourne shell
csh C shell : C언어를 기반으로 만들어졌으며, 강력한 프로그램 작성기능을 가지고 있다.
ksh Kron shell : 콘 쉘이라고 읽으며 C쉘을 기반으로 업그레이드 한 쉘 중 하나이다.
tcsh TENEX C Shell : C쉘의 기능을 강화 시킨 쉘이다.
그렇다면 Shell Script란 무엇일까?
Shell Script란 Shell 이나 Command Line 인터프리터에서 구동되도록 작성된 스크립트입니다. 여러가지 명령어 조합을 수행하거나, 반복적인 명령어를 단일 명령으로 쉽게 사용할 수 있습니다.
(윈도우에서 .bat 파일을 들어보셨다면, 리눅스에서 사용되는 같은 개념이라고 생각하면 됩니다)
Unix 계열 Shell Script의 필수적인 구문이다. Script 파일의 최상단에 해당 파일을 해석해줄 인터프리터의 절대경로를 지정해주는 것이다. ( bash shell 인터프리터로 지정한 것을 의미)
#! /bin/bash
name="inpa" # 변수 선언 및 대입
pass=123123 # 따옴표로 감싸든 말든 문자열로 저장됨
echo $name
# {}가 있으나 없으나 $만으로 변수의 값을 넣어줄 수 있으나,
#문자열을 붙여서 쓸려면 ${} 를 사용해야 한다.
echo "my name is mr.${name}"
printf "%s" $pass
기본적으로 Bash 변수는 "문자열"만 저장한다.
하지만, 자료형 타입을 미리 지정해주는 문법도 존재한다.
(혼자서, 문법공부 먼저 안하고, 구현에 집중하느라.. 이게 무슨말이야 하면서 찾아보면서 개발했는데, 미리 개념을 한번 싹 잡고 시작했으면 좀 더 편했을 수도 있을거 같다.)
# -r 읽기 전용 타입 (상수 const라고 보면 된다)
declare -r var1
readonly var1
# -i 정수형 타입
declare -i number
number=3
echo "number = $number" # number = 3
# -a 배열 타입
declare -a indices
# -A 연관배열(MAP) 타입
declare -A map
# -f 함수 타입
declare -f
# -x 환경변수(export) 지정
declare -x var3 # 스크립트 외부 환경에서도 이 변수를 쓸 수 있게 해준다.
이 부분에서 제일 답답했던 것은 정수형 타입이였다.
shell Script에서 산술 연산을 어떻게 하는지 알아봅시다.
shell Script에서 산술연산의 방법은 총 3가지가 존재합니다.
사용법만 알면 간단하니, 예제만 올려두겠습니다.
number1=10
number2=20
plus=`expr $number1 + $number2`
minus=`expr $number1 - $number2`
mul=`expr $number1 \* $number2` # 곱셈에는 \* 를 이용한다.
div=`expr $number1 / $number2`
rem=`expr $number1 % $number2`
# 우선순위 산술 연산을 할때는 괄호를 문자 처리해야 한다.
# 연산자 *와 괄호() 앞에는 역슬래시와 같이 사용
num=`expr \( 3 \* 5 \) / 4 + 7`
echo $num
num1=42
num2=9
let re=num1+num2 #Add
echo "add:$re"
let re=num1-num2 #Sub
echo "sub:$re"
let re=num1*num2 #Mul
echo "mul:$re"
let re=num1/num2 #Div
echo "div:$re"
let re=num1%num2 #Mod
echo "mod:$re"
echo add:$((num1+num2))
echo sub:$((num1-num2))
echo mul:$((num1*num2))
echo div:$((num1/num2))
echo mod:$((num1%num2))
내가 봤을 때, let 방식이 가장 간단한 방식인거 같다..
다음으로, 조건문이다.
if 문의 특징은 조건식을 대괄호로 감싸고 조건식과 대괄호 사이에 공백이 존재해야한다는 점과, 조건문의 끝을 알리는 fi 가 필요하다는 점이다. 기본 틀을 보도록 하자
if [(공백필요) (조건식) (공백필요) ]
then
수행
else
수행
fi
# 가독성 좋기 위해 then을 if [] 와 붙여쓰려면 반드시 세미콜론 ; 을 써야한다.
if [ 값1 조건식 값2 ]; then
수행1
else
수행2
fi
if 문 사용시, 이중괄호를 사용하면, 비교표현식이 들어갈 수 있다.
( 우리가 자주 사용하는 ! ~ << >> & | && || num++ 등등)
기본적인 for문 작성하는 것도 문법이 조금 난해하다 보니 고생했다. 잘 살펴보도록 하자
# 초기값; 조건값; 증가값을 사용한 정통적인 for문
for ((i=1; i<=4; i++)); do
echo $i
done
다른 예시
declare -a test
test[0]=123
# 배열 길이 구하고,
length=${#test[@]}
# 배열길이만큼 돌면서 index에 해당하는 값 찍기.
for ((i=0; i<${#test[@]}; i++)); do
echo "value : ${test[i]}"
done
프로그램은 조건문, 반복문으로 구성된 명령들의 집합이라고 봐도 무방하다고 생각한다. 기본을 알았으니, 우선 이렇게만 정리해두고, 이제 내가 짠 코드에서 사용된 부분들중 모르는 것을 정리해보도록 하겠다.
내가 CSV로 제공되는 ErrorMessage를 자동화 하여 Java 코드에서 사용하도록 간단하게 만들어본 shell script 이다.
input_file="./error_message.csv"
output_file="./src/main/java/kr/co/wizcore/wcm/web/rest/errors/constant/ErrorMessage.java"
userInput="public class ErrorMessage{
}"
messageKeyColumnIndex=5
messageValueColumnIndex=6
csvKeyArray=()
csvValueArray=()
javaFileKeyArray=()
javaKeyPrefix="public static final String"
echo "==================="
echo "ErrorMessage.java 파일 읽는중..."
if [ -f "$output_file" ];
then
while IFS= read -r line
do
line=$(echo -n "$line" | tr -d '[:cntrl:]')
if [ -z "$line" ]; then
continue
fi
if [[ $line == *"${javaKeyPrefix}"* ]];
then
substring="${line#*$javaKeyPrefix }"
IFS='='
read -ra substrings <<< "$substring"
javaFileKey=$(echo -n "${substrings[0]}"|tr -d '[:space:]')
javaFileKeyArray+=("$javaFileKey")
fi
done < "$output_file"
echo "java key length : ${#javaFileKeyArray[@]}"
fi
# output 파일 없으면 생성
if [ -f "$output_file" ]; then
echo "File $output_file exists."
else
touch "$output_file"
echo "$userInput" > "$output_file"
fi
echo "==================="
echo "error_message.csv 파일 읽는중..."
if [ ! -f "$input_file" ];
then
echo "ErrorMessage INPUT CSV does not exist, Check it plz"
exit
fi
# Read the CSV file line by line 구분자
while IFS= read -r line
do
line=$(echo -n "$line" | tr -d '[:cntrl:]' )
if [ -z "$line" ]; then
continue
fi
IFS=','
read -ra values <<< "$line"
# Split 한 내용 key,value 로대입
key=${values[$messageKeyColumnIndex]}
value=${values[$messageValueColumnIndex]}
csvKey=$(echo -n "$key" | tr -d '[:space:]')
#중복체크 로직
if [ -z "$csvKey" ] || echo "${javaFileKeyArray[@]}" | grep -qw "$csvKey" || echo "${csvKeyArray[@]}" | grep -qw "$csvKey"
then
continue
fi
csvKeyArray+=("$key")
csvValueArray+=("$value")
done < "$input_file"
echo "error_message.csv 파일 완료"
echo "==================="
echo "==================="
echo "ErrorMessage 파일에 작성하는중 ..."
file_end_pattern='}'
index=1
arrayLength=${#csvKeyArray[@]}
echo "추가되는 항목 수 : $(($arrayLength-1))"
for ((i=index; i<${arrayLength}; i++)) do
echo "추가되는 KEY : ${csvKeyArray[i]}"
done
while [ "$index" -lt "$arrayLength" ]
do
content_to_insert="${javaKeyPrefix} ${csvKeyArray[$index]}=\"${csvValueArray[$index]}\";"
content_to_insert=$(echo -n "$content_to_insert" | tr -d '[:cntrl:]' )
sed -i "/$file_end_pattern/i $content_to_insert" "$output_file"
index=$((index+1))
done
echo "ErrorMessage 파일에 완료"
echo "==================="
코드를 보면서 헷갈렸던 부분들을 추려보자
1 IFS ?
2 line" | tr -d '["cntrl:]')
3 이중대괄호 [[ ]]
4 sed -i "/file_end_pattern/i $content_to_insert" "output_file"
5 csvKeyArray+=("$key")
이 다음글에서는, 위의 헷갈리는 5가지 문법에 대해 정리하는 글과, 내가 만든 스크립트 사용시 유의사항과 개선점에 대해 고민해보도록 하겠다.