
bash : Bourne Again SHell
bash 셸은 명령어를 해석해서 실행해준다.
bash 셸의 특징
- Alias - 명령어 단축 기능
- History 기능 - 위/아래 단축키
- 연산 기능
- Job Control 기능
- 자동 이름 완성 기능 (탭키)
- 프롬프트 제어 기능
- 명령 편집 기능
셸의 명령문 처리 방법
- (프롬프트) 명령어 [옵션...][인자...]
- rm -rf /mydir
#!/bin/bash
"echo $환경변수이름" # 환경변수 확인
"export 환경변수=값" # 환경변수 값 변경
| 환경변수 | 설명 | 환경변수 | 설명 |
|---|---|---|---|
HOME | 사용자의 홈 디렉토리 경로를 지정합니다. | LANG | 프로그램의 언어 설정을 지정합니다. |
TERM | 터미널 타입을 지정합니다. | USER | 현재 사용자의 이름을 지정합니다. |
COLUMNS | 터미널의 열 수를 지정합니다. | PS1 | 프롬프트에서 표시되는 첫 번째 문자열을 지정합니다. |
BASH | Bash 쉘의 경로를 지정합니다. | HISTFILE | 히스토리 파일의 경로를 지정합니다. |
HOSTNAME | 시스템의 호스트 이름을 지정합니다. | LOGNAME | 현재 사용자의 로그인 이름을 지정합니다. |
MAIL | 메일함의 경로를 지정합니다. | PATH | 실행 파일이 위치한 디렉토리들을 쉼표로 구분하여 지정합니다. |
PWD | 현재 작업 중인 디렉토리의 경로를 지정합니다. | SHELL | 현재 사용 중인 쉘의 경로를 지정합니다. |
DISPLAY | X 윈도 시스템 디스플레이의 주소를 지정합니다. | LINES | 터미널의 행 수를 지정합니다. |
PS2 | 프롬프트에서 표시되는 두 번째 문자열을 지정합니다. | BASH_VERSION | Bash 쉘의 버전을 지정합니다. |
HISTSIZE | 히스토리 파일에 저장되는 명령어의 최대 개수를 지정합니다. | USERNAME | 현재 사용자의 이름을 지정합니다. |
LS_COLORS | ls 명령의 색상 설정을 지정합니다. | OSTYPE | 현재 운영 체제의 타입을 지정합니다. |
개인적으로 무언가 학습할때 글로만, 말로만 듣고서는 이해가 잘 안되는 편이다.
그래서 하나씩 직접 확인해봤다.
리눅스에서도 확인이 가능하겠지만 맥 os 를 쓰고 있기도 하고 visual studio code 로 하나씩 쳐봤다.
| 환경변수 | 설명 | 환경변수 | 설명 |
|---|---|---|---|
HOME | 사용자의 홈 디렉토리 경로를 지정합니다. | LANG | 프로그램의 언어 설정을 지정합니다. |
TERM | 터미널 타입을 지정합니다. | USER | 현재 사용자의 이름을 지정합니다. |
COLUMNS | 터미널의 열 수를 지정합니다. | PS1 | 프롬프트에서 표시되는 첫 번째 문자열을 지정합니다. |
BASH | Bash 쉘의 경로를 지정합니다. | HISTFILE | 히스토리 파일의 경로를 지정합니다. |
HOSTNAME | 시스템의 호스트 이름을 지정합니다. | LOGNAME | 현재 사용자의 로그인 이름을 지정합니다. |
MAIL | 메일함의 경로를 지정합니다. | PATH | 실행 파일이 위치한 디렉토리들을 쉼표로 구분하여 지정합니다. |
PWD | 현재 작업 중인 디렉토리의 경로를 지정합니다. | SHELL | 현재 사용 중인 쉘의 경로를 지정합니다. |
DISPLAY | X 윈도 시스템 디스플레이의 주소를 지정합니다. | LINES | 터미널의 행 수를 지정합니다. |
PS2 | 프롬프트에서 표시되는 두 번째 문자열을 지정합니다. | BASH_VERSION | Bash 쉘의 버전을 지정합니다. |
HISTSIZE | 히스토리 파일에 저장되는 명령어의 최대 개수를 지정합니다. | USERNAME | 현재 사용자의 이름을 지정합니다. |
LS_COLORS | ls 명령의 색상 설정을 지정합니다. | OSTYPE | 현재 운영 체제의 타입을 지정합니다. |
AMI 에서 ubuntu 나 linux 의 bash 셸을 사용했을 때와 visual studio code 에서 zsh 를 사용했을 때의 결과가 약간 다르게 나타났다. 그래도 기본적으로는 거의 유사한 결과를 볼 수 있었다.

#!/bin/bash
val="Yes Sir"
echo $val # Yes Sir
val=7+5
echo $val # 7+5
#!/bin/bash
echo 값 입력 :
read myvar
echo '$myvar' = $myvar # 입력한게 test 였다면 $myvar = test 출력
#!/bin/bash
num1=100
num2=$num1+200
echo $num2 # 100+200
num3=`expr $num1 + 200`
echo $num3 # 300
num4=`expr \($num1 + 200 \) / 10 \* 2`
echo $num4 # 60 ?
num5=$((($num1 + 200) / 10 * 2))
echo $num5 # 60
num6=`expr \( $num1 + 200 \) / 10 \* 2`
echo $num6 # 60
#!/bin/bash
# if [ 조건 ]; then
# 참일 경우 실행
# fi
if [ "A" = "A" ]; then # 셸에서는 = 나 == 나 둘다 동등연산자처럼 같은지 비교할때 사용 가능하다.
echo "true"
fi
# if [ 조건 ]; then
# 참일 경우 실행
# elif
# temp
# else
# temp
# fi
찾다보니 bash script 에서의 연산자를 잘 모르고 있어서 이 기회에 한번 확인해봤다.
정수 비교
| 연산자 | 의미 | 표현 |
|---|---|---|
| -eq | 같음 | $a -eq $b |
| -ne | 같지 않음 | $a -ne $b |
| -gt | 보다 큼 | $a -gt $b |
| -ge | 이상 | $a -ge $b |
| -lt | 미만 | $a -lt $b |
| -르 | 작거나 같음 | $a -le $b |
| > | 보다 큼(숫자 비교) | [[ $a > $b ]] |
| >= | 크거나 같음(숫자 비교) | [[ $a >= $b ]] |
| < | 미만(숫자 비교) | [[ $a < $b ]] |
| <= | 작거나 같음(숫자 비교) | [[ $a -le $b ]] |
문자열 비교
| 연산자 | 의미 | 표현 |
|---|---|---|
| = | Equal | "$string1" = "$string2" |
| == | Equal | "$string1" == "$string2" |
| != | Not equal | "$string1" != "$string2" |
| -z | Zero length | -z "$string" |
| -n | Non-zero length | -n "$string" |
| > | Greater than | "$string1" > "$string2" |
| < | Less than | "$string1" < "$string2" |
-n, -z 의 경우 각각 문자열이 존재하는지, 문자열이 없는지 등의 조건을 사용하는데에 자주 사용되는 비교연산자들이라 잘 알고 있어야 한다.
참고
[[ "$a" == a* ]]: $a 가 "a" 로 시작하면 참 (패턴)[[ "$a" == "a*" ]]: $a 가 "a*" 와 같으면 참[ "$a" == a* ]: 잘못된 표현[ "$a" == "a*" ]: $a 가 "a*" 와 같으면 참
=> 셸을 쭉 써오면서 변수를 사용하는데 어떤 경우 "" 로 감쌀 때와 그렇지 않을때 차이가 발생하는 부분에 대해 왜 그런 것인지 의문을 가졌었다. 그 각각의 케이스를 모두 나타내지는 않는 예시지만 일부 의문점이 해소될 수 있는 부분이다.
복합 비교
| Operator | Meaning | Expression |
|---|---|---|
| -a | Logical AND | if [[ condition1 -a condition2 ]] |
| -o | Logical OR | if [[ condition1 -o condition2 ]] |
| && | Logical AND | if [[ command1 && command2 ]] |
| || | Logical OR | if [[ command1 || command2 ]] |
파일 비교
| 연산자 | 의미 | 표현식 |
|---|---|---|
| -d | 디렉토리 존재 여부 | if [[ -d $path ]] |
| -f | 파일 존재 여부 | if [[ -f $path ]] |
| -e | 존재 여부 | if [[ -e $path ]] |
| -r | 읽기 가능 여부 | if [[ -r $path ]] |
| -w | 쓰기 가능 여부 | if [[ -w $path ]] |
| -x | 실행 가능 여부 | if [[ -x $path ]] |
| -s | 크기 여부 | if [[ -s $path ]] |
| -O | 소유자 여부 | if [[ -O $path ]] |
| -G | 그룹 소유자 여부 | if [[ -G $path ]] |
| -nt | 최신 여부 | if [[ $file1 -nt $file2 ]] |
| -ot | 이전 여부 | if [[ $file1 -ot $file2 ]] |
셸에는 switch case 문이 아니라 case ~ esac 문이 있다.
#!/bin/bash
echo "Enter a fruit: "
read fruit
case $fruit in
apple)
echo "You chose an apple."
;;
banana)
echo "You chose a banana."
;;
orange|kiwi)
echo "You chose an orange or a kiwi."
;;
*)
echo "You chose something else."
;;
esac
#!/bin/bash
echo "Do you like programming? (yes/no)"
read answer
case $answer in
yes | y | Y | Yes | YES )
echo "Great! Programming is fun!"
;;
[nN]*)
echo "That's okay. Programming isn't for everyone."
;;
*)
echo "Please answer yes or no."
;;
esac
#!/bin/bash
# for 변수 in 값1 값2 값3 ...; do
# 반복할 명령어
# done
# Define an array of fruits
fruits=("apple" "banana" "orange" "kiwi" "grape")
echo "List of fruits:"
# Iterate over each fruit in the array using a for loop
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
for file in test.sh; do
echo "File: $file"
echo "---------------------"
# Print the first three lines of the file
head -3 "$file"
echo "---------------------"
done
#!/bin/bash
count=1
echo "Counting from 1 to 5 using a while loop:"
# Using a while loop to count from 1 to 5
while [ $count -le 5 ]; do
echo "Count: $count"
((count++)) # Increment the count variable
done
echo "Finished counting."
셸도 조건식 내에 1 혹은 : 를 붙이면 true 로 평가된다. 따라서 [1] 또는 [:] 가 조건식으로 사용되면 항상 참이된다.
#!/bin/bash
count=1
echo "Counting from 1 to 5 using an until loop:"
# Using an until loop to count from 1 to 5
until [ $count -gt 5 ]; do
echo "Count: $count"
((count++)) # Increment the count variable
done
echo "Finished counting."
break 는 반복문을 종료할 때 사용된다.continue 는 반복문의 조건식으로 돌아가게 한다.exit 는 해당 프로그램을 완전히 종료한다.return 은 함수 안에서 사용될 수 있고 함수를 호출한 곳으로 돌아가게 한다.#!/bin/bash
# Define a custom function
function greet() {
echo "Hello, $1!"
}
# Call the function with an argument
greet "Alice"
나는 개인적으로 셸에서도 호이스팅이 적용될지 궁금했다.
확인해봤을때는 적용되지는 않는 것 같았다.
그런데 function 정의 - 변수 정의 - 함수 호출의 순서일 경우
함수가 해당 변수를 사용할 경우 이 변수는 함수 정의문보다 앞서 선언 및 사용된 적이 없으나 함수 사용 시점은 변수 정의 이후에 발생한 이벤트라 문제없이 동작한다.
#!/bin/bash
# Define a variable containing a command
command="echo Hello, World!"
# Execute the command directly
echo "Executing command directly:"
$command
# Use eval to execute the command stored in a variable
echo "Using eval:"
eval "$command"
exp1.sh
#!/bin/bash
# Define an environment variable
export MESSAGE="Hello from exp1.sh!"
# Execute exp2.sh
./exp2.sh
exp2.sh
#!/bin/bash
# Access the environment variable defined in exp1.sh
echo "Message from exp2.sh: $MESSAGE"
#!/bin/bash
# Define variables
name="Alice"
age=30
height=165.5
# Using printf to format and print output
printf "Name: %s\n" "$name" # Name: Alice
printf "Age: %d\n" "$age" # Age: 30
printf "Height: %.1f\n" "$height" # Height: 165.5
#!/bin/bash
echo "오늘 날짜는 $(date) 입니다." # 오늘 날짜는 Sat Apr 13 00:50:18 KST 2024 입니다.
set $(date)
echo "오늘은 $1 요일 입니다." # 오늘은 Sat 요일 입니다.
#!/bin/bash
echo "Iterating through command-line arguments using shift:"
# Loop through all command-line arguments
while [ $# -gt 0 ]; do
echo "Argument: $1"
shift
done
나는 2023년 10월부터 2024년 4월 초까지 6개월간 백준 알고리즘 스터디를 운영해왔다.
그 과정에서 반복적으로 수행되는 작업들에 대해 셸을 통해 자동화할 수 없을까 하는 생각을 하게 되었고 먼저 반복적으로 해야 하는 작업들을 정리해봤다.
이 과정을 자동화했다.
먼저 어떻게 했을까?
필요한 작업은 다음과 같다.
되게 간단해 보이는데 실제로 구현하기까지 4주정도 걸렸다. ( 사실 주말중 일요일 하루만 투자해서 4주 정도 ... )
이 부분은 주차별 주제를 고려하고 적절성 등을 판단하여 뽑아야 했기 때문에 수작업으로 했다.
problems.txt
https://www.acmicpc.net/problem/15886
https://www.acmicpc.net/problem/16945
https://www.acmicpc.net/problem/16987
https://www.acmicpc.net/problem/16938
https://www.acmicpc.net/problem/7490
https://www.acmicpc.net/problem/3190
https://www.acmicpc.net/problem/12100
crawling.py
# developed by Jeonhui Lee
# pip3 install requests
# pip3 install bs4
'''
Baekjoon 문제 번호로 문제 불러오기
'''
import sys
def get_baekjoonproblem(problem_number):
import requests
from bs4 import BeautifulSoup as Soup
# problem_number = sys.stdin.readline().rstrip()
# 문제 번호 입력
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'}
soup = Soup(requests.get('https://www.acmicpc.net/problem/' + problem_number, headers=headers).text, 'html.parser')
# 문제 주소의 html 파일을 text로 불러옴
title = soup.find('span', {'id': 'problem_title'}).text
# id가 problem_title인 span태그를 찾음
return title
except Exception as e:
# 잘못된 입력이 들어왔을 시에 problem_tilte을 찾지 못함 -> 종료
print("잘못된 문제 번호입니다.", e)
return
if __name__ == '__main__':
problem_number = sys.argv[1]
problem_title=get_baekjoonproblem(problem_number)
print(problem_title)
위의 코드는 구글링중 Jeonhui Lee 님 블로그에 공유된 코드중 문제이름을 추출하는 부분만 사용한 코드다. 다시 구글링해보니 어디서 확인했는지 보기 어려워 링크를 못찾았다..
스터디를 운영하다보니 실제 코테에서는 프로그래머스 환경에서 치뤄지는 경우가 많다보니 매주 7문제씩 푸는 것중 한문제는 프로그래머스 문제를 선정했다.
그러다보니 백준 문제이름만 추출해서는 프로그래머스 문제 이름을 같이 추출하지는 못하는 문제가 있어 이 경우 역시 구글링으로 찾아봤다.
crawling_psg.py
# 참고한 레퍼런스 : https://velog.io/@a101201031/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AC%B8%EC%A0%9C-%ED%81%AC%EB%A1%A4%EB%A7%81%ED%95%B4%EC%84%9C-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4%EC%9C%BC%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B01
import os
import sys
import requests
from bs4 import BeautifulSoup
from markdownify import MarkdownConverter as MarkdownifyConverter
class MarkdownConverter:
_module_path = os.path.dirname(os.path.abspath(__file__))
def __init__(self, url, parser):
self.request_html(url)
self.convert_html_bs4(parser)
self.title = self.find_category_title()
def request_html(self, url):
res = requests.get(url)
self.html = res.text
def convert_html_bs4(self, parser):
self.soup = BeautifulSoup(self.html, "lxml")
def find_category_title(self):
category_title_soup = self.soup.find("ol", "breadcrumb")
_, _, tilte_soup = category_title_soup.find_all("li")
title = tilte_soup.get_text()
return title
if __name__ == "__main__":
url = "https://school.programmers.co.kr/learn/courses/30/lessons/60057"
if len(sys.argv) != 1:
url = sys.argv[1]
markdown_converter = MarkdownConverter(url, "lxml")
print(markdown_converter.title)
이 코드는 셸 문법을 공부해가며 직접 구현했다. 역시 gpt 의 도움을 많이 받았다.
create.sh
#!/bin/bash
# 형식 : ./create.sh {이름} {확장자} -> 확장자를 전달하지 않으면 py 파일을 생성합니다.
# 위의 명령어를 실행하면 현재 주차에 해당하는 자신이 생성해야 할 파일과 폴더들을 생성합니다.
# 스크립트 파일이 위치한 디렉토리 경로
script_dir=$(dirname "$(readlink -f "$0")")
# 프로젝트의 루트 디렉토리 경로
root_dir=$(dirname "$script_dir")
# 현재 날짜의 주차 정보 가져오기
current_week=$((10#$(date +%U)))
study_week=$((current_week + 11))
week_folder="$root_dir/week$study_week"
mkdir -p "$week_folder"
file="$root_dir/scripts/problems.txt"
executor_name="$1"
expansion="$2" # 두 번째 인자로 받은 확장자
# 파일을 한 줄씩 읽어 배열에 담기
IFS=$'\n' read -d '' -r -a lines < "$file"
# 배열 순회하여 각 줄 출력
total_boj=${#lines[@]}
length=$((total_boj - 1))
for (( i=0; i<${#lines[@]}; i++ )); do
link="${lines[$i]}"
# TODO: 19주차부터 지울 부분
problem_number=$(echo $link | cut -d '/' -f5 )
problem_title=$(python $script_dir/crawling.py $problem_number)
problem_folder="${week_folder}/BOJ_${problem_number}"
# # TODO: 19주차부터 PSG 에 적용
# if [[ $i < $length ]]; then
# problem_number=$(echo $link | cut -d '/' -f5 )
# problem_title=$(python $script_dir/crawling.py $problem_number)
# problem_folder="${week_folder}/BOJ_${problem_number}"
# else
# problem_title=$(python ./scripts/crawling_psg.py $link)
# problem_folder="${week_folder}/PSG_${problem_title}"
# fi
mkdir -p "$problem_folder"
# 두 번째 인자가 주어졌을 때 해당 확장자로 파일 생성
if [ -n "$expansion" ]; then
touch "${problem_folder}/${problem_title}_${executor_name}.${expansion}"
else
# 두 번째 인자가 주어지지 않았을 때 기본적으로 py 확장자로 파일 생성
touch "${problem_folder}/${problem_title}_${executor_name}.py"
fi
done
스터디원들이 파이썬, 자바스크립트, c++ 로 코테를 준비하는 인원들로 나뉘었기에 각자의 언어별로 생성할 수 있도록 분기처리했다.
실제 구현 결과는 백준 알고리즘 스터디 깃허브 링크를 참고하자.
나는 개인적으로 이 스크립트가 굉장히 유용했다. 스터디장이다보니 매주 반복적으로 해야하는 작업들이 있는데 그 부분에 대한 공수가 많이 줄었다.
매주 스터디에 참여하는 인원들이 고정적이지 않다.
이게 큰 문제다.
스터디때마다 문제를 할당할때 7문제에 7개의 이름을 적절히 분배해서 사다리타기를 하는데 갑자기 인원이 해당 주차에 참여할 수 없게 된다면 어떤 문제를 누구에게 할당해야 할지 머리가 아프다.
그리고 그렇게 할당한 문제는 또 노션에 스크린샷을 찍어 공지해야 한다..
귀찮은 일들이다...
그래서 명령어 한줄이면 문제를 바로 할당할 수 있고 이걸 바로 깃허브 read.md 에 반영되도록 했다.
굉장히 편리했다.
assign.sh
#!/bin/bash
# 변수 설정
exclude_members=("$@")
all_members=("스터디원1" "스터디원2" ... "홍지훈")
# 인자로 전달한 인원을 멤버에서 제외
for member in "${exclude_members[@]}"; do
all_members=("${all_members[@]//$member}")
done
all_members=($(echo "${all_members[@]}" | tr -s ' ' '\n' | grep -v '^$'))
# 인원 수 확인
num_members=${#all_members[@]}
num_problems=$((num_members > 2 ? 2 : num_members))
add_member=()
for ((i = 0; i < 7 - $num_members; i++)); do
random_index=$((RANDOM % ${#all_members[@]}))
add_member+=("${all_members[random_index]}")
done
all_members+=("${add_member[@]}")
# 스크립트 파일이 위치한 디렉토리 경로
script_dir=$(dirname "$(readlink -f "$0")")
# 프로젝트의 루트 디렉토리 경로
root_dir=$(dirname "$script_dir")
current_week=$((10#$(date +%U)))
study_week=$((current_week + 11))
file="$root_dir/scripts/problems.txt"
# 파일을 한 줄씩 읽어 배열에 담기
IFS=$'\n' read -d '' -r -a lines < "$file"
total_boj=${#lines[@]}
# length=$((total_boj - 1))
length=$((total_boj))
problems=()
# 배열 순회하여 각 줄 출력
for ((i = 0; i < ${#lines[@]}; i++)); do
link=${lines[$i]}
if [[ $i < $length ]]; then
problem_number=$(echo $link | cut -d '/' -f5 )
problem_title=$(python ./scripts/crawling.py $problem_number)
problems+=("${problem_number}_${problem_title}")
# else
# problem_title=$(python ./scripts/crawling_psg.py $link)
# problems+=("${problem_title}")
fi
done
# # exclude_members가 비어있는 경우
# if [[ -z "$exclude_members" ]]; then
# # 랜덤 숫자로 선택된 문제 추가
# random_problem_index=$((RANDOM % 7))
# problems+=("${problems[$random_problem_index]}")
# fi
# Fisher-Yates 알고리즘 -> 인원 랜덤 섞기
for i in "${!all_members[@]}"; do
j=$((RANDOM % ($i + 1)))
temp=${all_members[$i]}
all_members[$i]=${all_members[$j]}
all_members[$j]=$temp
done
# 결과 출력
echo "# $study_week 주차 문제 배정 결과" > weekly_assign.md
echo "" >> weekly_assign.md
# 문제의 수에 따라 표를 생성합니다.
printf "|" >> weekly_assign.md
for problem in "${problems[@]}"; do
printf "%s" " $problem |" >> weekly_assign.md
done
echo "" >> weekly_assign.md
printf "|" >> weekly_assign.md
for problem in "${problems[@]}"; do
printf "%s" " --- |" >> weekly_assign.md
done
echo "" >> weekly_assign.md
printf "|" >> weekly_assign.md
for member in "${all_members[@]}"; do
printf "%s" " $member |" >> weekly_assign.md
done
매주 problems.txt 만 업데이트해주고 ./assign.sh 만 실행해주면 자동으로 적절히 각 스터디원들에게 문제를 할당하고 깃허브 리드미에 공지해준다.

이런식으로 셸을 일상속에 활용해 볼 수 있었다.
만약 반복적으로 하는 일들이 있다면 어지간한 것은 모두 자동화하는데 셸이나 파이썬을 사용하여 해결 할 수 있을 것 같다.