Skip to content

파일트리 자동생성

레포지토리 폴더에 .git/hook/pre-commit 파일을 생성하여 아래 스크립트를 넣으면 커밋시에 README.md 파일에 트리가 자동으로 생성된다. (mac 기준)

#!/bin/bash
export LANG=UTF-8
function generate_project_tree() {
tree /Users/rlaisqls/Documents/github/TIL -tf --noreport -I '~' --charset ascii $1 |
gsed -e 's/[|]-\+/╊━/g' |
gsed -e 's/[|]/┃/g' |
gsed -e 's/[`]/┗━/g' |
gsed -e 's/[-]/━/g' |
gsed -e 's:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g' |
gsed -e 's/)/)<\/br>/g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL=./TIL</br>=g' |
gsed -e 's/━━━/━/g' |
gsed -e 's/[ ]/ /g'
}
function generate_readme() {
readme="/Users/rlaisqls/Documents/github/TIL/README.md"
readme_template="/Users/rlaisqls/Documents/github/TIL/.git/hooks/readme_template.md"
perl -p0e 's/__PROJECT_TREE__/`cat`/se' "$readme_template" > "$readme"
}
cd "$(dirname "$0")" || exit 1
generate_project_tree . | generate_readme .
cd /Users/rlaisqls/Documents/github/TIL
git add /Users/rlaisqls/Documents/github/TIL/README.md
git commit --amend -C HEAD --no-verify

.git/hook/readme-template.md도 만들어준다. __PROJECT_TREE__를 제외한 나머지부분은 꾸미고 싶은대로 바꿔도 된다.

레포지토리 파일트리
__PROJECT_TREE__

추가: 위 명령어대로 했을때 뭔가 정렬이 이상하다 싶었는데, tree 옵션 문제였다.

마지막 수정 시간별로 파일을 정렬하는 -t 옵션을 없애고, 파일보다 디렉토리가 더 위로 오도록 하는 --dirsfirst를 사용하면 딱 예쁘게 정렬할 수 있다.

#!/bin/bash
export LANG=UTF-8
function generate_project_tree() {
tree /Users/rlaisqls/Documents/github/TIL -f --dirsfirst --noreport -I '~' --charset ascii $1 |
gsed -e 's/[|]-\+/╊━/g' |
gsed -e 's/[|]/┃/g' |
gsed -e 's/[`]/┗━/g' |
gsed -e 's/[-]/━/g' |
gsed -e 's:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g' |
gsed -e 's/)/)<\/br>/g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL=./TIL</br>=g' |
gsed -e 's/━━━/━/g' |
gsed -e 's/[ ]/ /g'
}
function generate_readme() {
readme="/Users/rlaisqls/Documents/github/TIL/README.md"
readme_template="/Users/rlaisqls/Documents/github/TIL/.git/hooks/readme_template.md"
perl -p0e 's/__PROJECT_TREE__/`cat`/se' "$readme_template" > "$readme"
}
cd "$(dirname "$0")" || exit 1
generate_project_tree . | generate_readme .

지난번에 이 레포지토리에 파일트리를 자동으로 생성하는 git hook을 만들었었다.

README에 파일 목록이 딱 보이니 어떤 내용들을 공부했었는지 파악하기도 쉽고, 깔끔해져서 정말 만족스러웠다.

그런데 문득, 이 레포지토리에 적은 글의 수 같은 정보를 함꼐 보여주면 더 좋지 않을까? 하는 생각이 들었다.

그래서 README와 git hook에 내용을 더 추가해보기로 했다! 일단 추가해본 부분은 파일(글)의 갯수, 폴더 갯수, 평균 글 길이이다. 파일 갯수는 내가 지금까지 몇가지의 내용을 공부했는지를 알 수 있고, 평균 글 길이는 얼마나 열심히 길게 적고있는지를 나타낼 수 있는 숫자이니 보고 (내가) 동기부여를 받을 수 있을 것 같아서 만들어보았다.

변경된 readme-template.md는 아래와 같다. backtic은 작은따옴표로 치환헀다.

# TIL
'''
files : __FILE_COUNT__
derectories : __DERECTORY_COUNT__
avg_file_length : __AVG_FILE_LEGNTH__
'''
__PROJECT_TREE__

pre-commit 코드는 파트별로 살펴보자.

이부분은 이전에도 있었듯이, tree 명령어 출력 결과를 링크가 달린 md 형식으로 변환해주는 코드이다. filepath 부분은 변수로 치환해서 공통으로 사용할 수 있도록 해줬다.

#!/bin/bash
export LANG=UTF-8
filepath=/Users/rlaisqls/Documents/github/TIL
function generate_project_tree() {
tree $filepath -f --dirsfirst --noreport -I '~' --charset ascii $1 |
gsed -e 's/[|]-\+/╊━/g' |
gsed -e 's/[|]/┃/g' |
gsed -e 's/[`]/┗━/g' |
gsed -e 's/[-]/━/g' |
gsed -e 's:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g' |
gsed -e 's/)/)<\/br>/g' |
gsed -e 's=$filepath/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL=./TIL</br>=g' |
gsed -e 's/━━━/━/g' |
gsed -e 's/[ ]/ /g'
}

파일과 폴더의 숫자를 세는 것은 find와 wc 명령어를 사용했다. find 명령어로 각각 f, d 타입의 요소들을 리스트로 출력하면 wc -ㅣ로 행의 수를 세서 출력하는 형식이다. 그리고 깔끔한 출력을 위해 gsed로 공백과 줄바꿈을 없앴다.

Terminal window
function generate_project_file_count() {
find $filepath -type f \( -name "*.md" \) |
wc -l |
gsed -e 's/[ ]//g' |
gsed -e 's/\n//g'
}
function generate_project_derectory_count() {
find $filepath -type d ! -path "*/.git/*" |
wc -l |
gsed -e 's/[ ]//g' |
gsed -e 's/\n//g'
}

글의 평균 길이를 측정하는 것도 find 명령어를 사용했다. 그 목록에 해당하는 글의 내용을 xargs cat이 받아서 전부 출력하면 wc 명령어가 글자 수를 세주는 구조이다. 파일 수가 점점 많아지면 이 과정에서의 메모리 부하가 심해지겠지만 아직 걱정할 수준은 아닌 것 같다. 현재는 내용이 총 50만 글자정도 되는데, 1억이 넘어가면 그떄부터 최적화를 생각해봐도 될 것 같다.

Terminal window
function file_legnth_total() {
find $filepath -type f ! -path "*/.git/*" ! -path "*/README.md" |
xargs cat |
wc -m |
gsed 's/ //g' |
gsed -e 's/\n//g'
}
function generate_avg_file_legnth() {
total=$(file_legnth_total)
count=$(generate_project_file_count)
echo $(expr $total / $count)
}

readme 파일을 생성하는 부분이다. 원래는 함수 호출 부분이 하나라서 밑부분에서 호출했었는데, 인자가 여러개가 되어서 그냥 이 함수에 다 합쳐줬다. cp로 템플릿 내용을 readme에 복사해놓고, 변수가 들어가야하는 부분을 하나씩 채워주는 흐름이다.

기존에는 템플릿에 변수를 대입한 내용을 만들어놓고 >를 사용해서 실제 readme 파일로 옮겨주었다. 근데 인자가 여러개가 되니까 하나를 대입해넣은 readme 파일에 또 덮어쓰기를 해야하는데, $readme > $readme와 같은 형식으로 코드를 작성하면 파일 내용이 자꾸 날아가서 명령어에 -i 옵션을 붙여서 readme 파일을 바로 수정하도록 변경했다.

Terminal window
function generate_readme() {
readme="$filepath/README.md"
readme_template="$filepath/.git/hooks/readme_template.md"
cp -f "$readme_template" "$readme"
generate_project_tree . |
perl -p0e 's/__PROJECT_TREE__/`cat`/se' -i $readme
generate_project_file_count . |
perl -p0e 's/__FILE_COUNT__/`cat`/se' -i $readme
generate_project_derectory_count . |
perl -p0e 's/__DERECTORY_COUNT__/`cat`/se' -i $readme
generate_avg_file_legnth . |
perl -p0e 's/__AVG_FILE_LEGNTH__/`cat`/se' -i $readme
}

이 부분이 함수를 호출하는 부분이고 매번 변경된 readme파일을 커밋하기가 좀 귀찮아져서 자동으로 전 커밋에 amend 해서 올라가도록 해줬다.

전 방식에선 해당 커밋이 아닌 바로 전 커밋에 amend 되어서 항상 force push를 해야하고 커밋 히스토리가 더러워진다는 단점이 있어서, git add만 하도록 수정도 해줬다.

Terminal window
generate_readme .
cd $filepath
git add $filepath/README.md

전체 코드이다.

#!/bin/bash
export LANG=UTF-8
filepath=/Users/rlaisqls/Documents/github/TIL
function generate_project_tree() {
tree $filepath -f --dirsfirst --noreport -I '~' --charset ascii $1 |
gsed -e 's/[|]-\+/╊━/g' |
gsed -e 's/[|]/┃/g' |
gsed -e 's/[`]/┗━/g' |
gsed -e 's/[-]/━/g' |
gsed -e 's:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g' |
gsed -e 's/)/)<\/br>/g' |
gsed -e 's=$filepath/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL=./TIL</br>=g' |
gsed -e 's/━━━/━/g' |
gsed -e 's/[ ]/ /g'
}
function generate_project_file_count() {
find $filepath -type f \( -name "*.md" \) |
wc -l |
gsed -e 's/[ ]//g' |
gsed -e 's/\n//g'
}
function generate_project_derectory_count() {
find $filepath -type d ! -path "*/.git/*" |
wc -l |
gsed -e 's/[ ]//g' |
gsed -e 's/\n//g'
}
function file_legnth_total() {
find $filepath -type f ! -path "*/.git/*" ! -path "*/README.md" |
xargs cat |
wc -m |
gsed 's/ //g' |
gsed -e 's/\n//g'
}
function generate_avg_file_legnth() {
total=$(file_legnth_total)
count=$(generate_project_file_count)
echo $(expr $total / $count)
}
function generate_readme() {
readme="$filepath/README.md"
readme_template="$filepath/.git/hooks/readme_template.md"
cp -f "$readme_template" "$readme"
generate_project_tree . |
perl -p0e 's/__PROJECT_TREE__/`cat`/se' -i $readme
generate_project_file_count . |
perl -p0e 's/__FILE_COUNT__/`cat`/se' -i $readme
generate_project_derectory_count . |
perl -p0e 's/__DERECTORY_COUNT__/`cat`/se' -i $readme
generate_avg_file_legnth . |
perl -p0e 's/__AVG_FILE_LEGNTH__/`cat`/se' -i $readme
}
cd "$(dirname "$0")" || exit 1
generate_readme .
cd $filepath
git add $filepath/README.md

간단한 추가지만, 작업하는데 거의 3-4시간정도 걸린 것 같다…

bash나 perl 명령어에 대한 이해도가 낮아서 삽질을 정말 많이 했다. 다음에 git hook으로 만들고싶은게 또 생긴다면 그떄는 더 잘할 수 있지 않을까 기대해본다!


큰 변화는 아니지만 작게 수정할 요소가 있어 새 버전의 소스코드를 만들었다.

1. 줄바꿈 처리

Terminal window
# 변경 전
...
gsed -e 's:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g' |
gsed -e 's/)/)<\/br>/g' |
...

이전에는 md에 줄바꿈을 적용하기 위해서 닫는 괄호인 ) 문자 뒤에 <br/>을 출력하도록 했는데, 파일명에 )가 들어가는 경우 불필요한 줄바꿈이 들어가서, <\/br>을 md link 형식으로 format을 바꿔주는 표현식 뒤에 붙였다.

Terminal window
# 변경 후
...
gsed -e "s:\(━ \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2)<\/br>:g" |
...

둘을 별도의 표현식으로 분리하려다가, sed의 capture group에 대해서도 같이 찾아보게 되었다.

  • sed 's/^\((.*\))$/(\1)</br>/'

이런 식으로도 구현 가능하지만 합치는게 더 간결한 것 같다.

2. 변수 처리

Terminal window
# 변경 전
filepath=/Users/rlaisqls/Documents/github/TIL
...
gsed -e 's=/Users/rlaisqls/Documents/github/TIL/=./=g' |
gsed -e 's=/Users/rlaisqls/Documents/github/TIL=./TIL</br>=g' |
...

gsed로 string replace 하는 부분에 filepath 변수가 적용되어있지 않아 수정해주었다.

Terminal window
# 변경 후
...
gsed -e "s=$filepath/=./=g" |
gsed -e "s=$filepath=./TIL</br>=g" |
...

처음에는 그냥 따옴표에 $로 변수를 설정했는데 안되어서 구글링해보았더니 그 부분에서 따옴표와 쌍따옴표의 차이가 있다고 한다.

Single Quotes은 그냥 character를 그대로 인식하고, 쌍따옴표인 Double Quotes은 $, \, ! 등의 표현을 사용할 수 있다.

Terminal window
$ echo "$(echo "upg")"
upg
$ echo '$(echo "upg")'
$(echo "upg")

gnu 공식 문서에도 이러한 내용이 설명되어있다.

3.1.2.2 Single Quotes

Enclosing characters in single quotes (’) preserves the literal value of each character within the quotes. A single quote may not occur between single quotes, even when preceded by a backslash.

3.1.2.3 Double Quotes

Enclosing characters in double quotes (”) preserves the literal value of all characters within the quotes, with the exception of $, , \, and, when history expansion is enabled, !. The characters $ and retain their special meaning within double quotes (see Shell Expansions). The backslash retains its special meaning only when followed by one of the following characters: $, `, ”, , or newline. Within double quotes, backslashes that are followed by one of these characters are removed. Backslashes preceding characters without a special meaning are left unmodified. A double quote may be quoted within double quotes by preceding it with a backslash. If enabled, history expansion will be performed unless an ! appearing in double quotes is escaped using a backslash. The backslash preceding the ! is not removed.

The special parameters * and @ have special meaning when in double quotes (see Shell Parameter Expansion).


cron시 명령어를 못 찾는 오류가 있어 PATH 환경변수를 추가해주었다.

export PATH=/opt/homebrew/bin:$PATH

참고