본문으로 건너뛰기

go 언어로 TIL-CLI 만들기

· 약 14분
arch-spatula

go 언어로 CLI를 만드는 여정을 공유합니다.

여기 이슈에 모르는 것을 작성하고 모르는 것을 찾고 적용해보고 정리하고 커밋하는 방식으로 작업했습니다. 질문주도 개발방법론을 적용했습니다.

문제 정의

개발의 목표는 제가 매번 수동으로 작성하는 TIL을 커맨드라인으로 자동화하는 것입니다.

현재는 프로토타입 수준으로 만들었습니다. 저를 위한 MVP를 빨리 뽑는 것이 중요합니다.

TIL-CLI today # 이번달 폴더 안에 오늘 TIL을 만듬
TIL-CLI tomorrow # 이번달 폴더 안에 내일 TIL을 만듬
TIL-CLI temp # today, tomorrow가 복사할 템플릿을 만듬

커맨드는 더 자세한 매개변수를 안 받고 그냥 단순한 것에서 시작하고자 합니다.

무슨 라이브러리로 go CLI를 만드는가?

Cobra를 많이 활용합니다.

Cobra가 유명한 go CLI 개발 라이브러리인 것 같습니다. 검색했을 때 지금까지 개발한 결과 자료가 많았습니다.

go mod 선언하는 방법은 무엇인가? 어떻게 패키지를 명명해야 하는가?

go 언어로 프로젝트 초기 설정할 때 go mod init 하는 형식이 생각 안났습니다.

레포를 기준으로 go mod init하면 됩니다.

go mod init github.com/(제작자)/(레포명)

저는 이번 프로젝트에 아래와 같이 선언했습니다.

go mod init github.com/arch-spatula/TIL-CLI

go get -u github.com/(제작자)/(레포명)에서 -u 플래그는 무엇인가?

-u 플래그는 최신을 의미합니다.

Cobra 공식문서에서 아래처럼 설치할 것을 권장합니다.

go get -u github.com/spf13/cobra/cobra

여기서 의문이 여기서 출발했습니다.

go get 명령은 Go 언어의 패키지 관리자 중 하나로, 외부 패키지를 가져오는 데 사용됩니다. -u 플래그는 "update"의 약자이며, go get 명령을 사용할 때 해당 패키지를 현재 버전에서 가장 최신 버전으로 업데이트하도록 지시하는 역할을 합니다.

구체적으로 go get -u github.com/spf13/cobra 명령은 GitHub에서 spf13/cobra 패키지를 가져오고, -u 플래그로 인해 이미 로컬에 설치된 패키지의 버전보다 최신 버전이 있다면 해당 패키지를 업데이트합니다.

이것은 종속성 관리 및 프로젝트의 패키지 버전을 최신 상태로 유지하는 데 유용한 방법 중 하나입니다.

- chatGPT

이렇게 알게되었습니다. 저는 위 인용문으로 제의 글의 신뢰를 잃었습니다.

위처럼 직접 설치해 괜찮습니다. 실제 코드에서 일부를 활용하고 있으면 go tidy를 사용해서 자동 설치하기 바랍니다.

go tidy
go install github.com/spf13/cobra-cli@latest

go 언어로 설치하는 다른 방법으로 위 명령도 존재합니다.

이제 본인 로컬 환경에 cobra-cli를 설치한 것입니다.

cobra-cli

위명령을 한번 시도해보고 무엇을 출력하는지 확인해보기 바랍니다.

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
cobra-cli [command]

Available Commands:
add Add a command to a Cobra Application
completion Generate the autocompletion script for the specified shell
help Help about any command
init Initialize a Cobra Application

Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra-cli
-l, --license string name of license for the project
--viper use Viper for configuration

대략 위와 같은 출력을 보면 설치가 성공한 것입니다.

설정하는 방법

위처럼 설치해도 아래와 같은 에러가 발생할 수 있습니다.

zsh: command not found: cobra

가장 훌륭한 답변을 찾았습니다.

-bash: cobra: command not found

제일먼저 .zshrc을 설정합니다.

cd && vim .zshrc

다음 아래 3줄을 붙여 넣고 저장해주세요.

export GOPATH=$HOME/go
export GOBIN=$GOPATH/bin
export PATH=${PATH}:$GOBIN

그리고 다음 명령으로 zsh을 실행해주세요.

source ~/.zshrc

터미널 재시작합니다.

cobra-cli

위로 명령으로 설정을 확인합니다. 만약에 위 방식으로 처리해도 아무런 효과가 없었다면 저도 모르겠습니다.

프로젝트 초기화 방법?

cobra-cli init .

저는 현재 root 디렉토리를 기준으로 CLI를 초기화하도록 했습니다.

본인 라이센스를 추가하는 방법?

평소 생각을 자주 안 하는 부분입니다. 하지만 본인이 만든 CLI에 대해서 라이센스를 코드로 작성하는 방법이 있습니다.

cobra-cli init -a "arch-spatula" -l MIT

저는 위처럼 작성했습니다.

cobra-cli init -a "저자" -l "라이센스 유형"

위처럼 작성하기 바랍니다. 본인 CLI를 본인이 보호하기 바랍니다.

명령 추가하는 방법?

cobra-cli add today

위 명령을 추가하면 됩니다. cmd 폴더에 today.go가 생성될 것입니다.

go run main.go today # today called

위처럼 실행하면 today called를 볼 수 있을 것입니다.

이제 더 고급스럽게 실행파일을 활용해서 명령해보겠습니다.

go build

실행파일을 빌드하기 바랍니다.

./TIL-CLI

이제 실행파일을 실행하면 사용할 수 있는 명령들을 볼 수 있습니다.

./TIL-CLI today # today called

실행파일의 커맨드 기능을 활용할 수 있습니다.

go build로 만든 실행파일 무시하기

# Ignore all
*

# Unignore all with extensions
!*.*

# Unignore all dirs
!*/

### Above combination will ignore all files without extension ###

# Ignore files with extension `.class` & `.sm`
*.class
*.sm

# Ignore `bin` dir
bin/
# or
*/bin/*

# Unignore all `.jar` in `bin` dir
!*/bin/*.jar

# Ignore all `library.jar` in `bin` dir
*/bin/library.jar

# Ignore a file with extension
relative/path/to/dir/filename.extension

# Ignore a file without extension
relative/path/to/dir/anotherfile

chichunchen/.gitignore을 참고했습니다.

현재 시간 출력하기

s := time.Now().Format("2006-01-02 15:04:05")
fmt.Println(s) // 2019-01-12 10:20:30

위 형식을 편집해서 이번달 폴더를 만드는 방법과 오늘 만들 TIL 파일 이름을 작성할 수 있습니다.

파일을 생성하는 방법

How to create new file using go script

func writeFile(filename, line string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()

_, err = fmt.Fprintln(file, line)
return err
}

폴더를 생성하는 방법

package main

import (
"log"
"os"
)

func main() {
//Create a folder/directory at a full qualified path
err = os.Mkdir("temp", 0755)
if err != nil {
log.Fatal(err)
}
}

Create a directory or folder in Go (Golang)

os.Create는 파일을 생성하도록 시스템에 요청하는 방법입니다. 위 방법을 응용하고 적용했습니다.

그리고 폴더(2310)를 먼저 생성하게 만들고 그리고 파일(TIL231014.md)을 생성하게 만들었습니다.

파일 존재 확인하기

파일의 존재를 확인하는 이유는 템플릿이 이미 만들어졌는데 덮어쓰는 문제가 발생할 수 있습니다. 또 오늘 TIL을 만들었는데 덮어쓰는 문제가 발생할 수 있습니다. 문제를 방지해야 합니다.

How to check if a file exists in Go?

if _, err := os.Stat("template.md"); errors.Is(err, os.ErrNotExist) {
fmt.Println("파일이 존재하지 않습니다.j")
}

에러가 발생하면 파일이 존재하지 않는 것입니다. 그래서 존재하지 않으면 생성하고 존재하면 생성하지 않고 이미 있다고 피드백만 주면 됩니다.

Golang Program to check a directory is exist or not

파일 내용 읽기

template.md에서 먼저 읽고 2310/TIL231014.md에 쓰기를 해야 합니다. 파일 내용을 읽을 때 존재를 먼저 확인해야 합니다.

파일 쓰기

template.md을 읽고 오늘 혹은 내일 생성하는 파일에 쓰기를 해야 합니다. todayMarkdownFileTIL231014.md을 문자열로 담고 있습니다. 이 파일을 생성하고 거기에 # hello today을 쓴 것입니다.

file, err := os.Create(todayMarkdownFile)
if err != nil {
fmt.Printf("Unable to write file: %v\n", err)
}
defer file.Close()

fmt.Fprintln(file, "# hello today")

위처럼 파일에 쓰기를 할 수 있습니다. 하지만 먼저 template.md를 열어야 합니다.

// template.md 읽기
template, err := ioutil.ReadFile("template.md")
if err != nil {
fmt.Printf("Unable to read file: %v\n", err)
}

이렇게 해서 파일 읽기를 만들 수 있습니다. 파일을 읽는 방법은 다양합니다.

todayFile, err := os.Create(todayMarkdownFile)
if err != nil {
fmt.Printf("Unable to write file: %v\n", err)
}
defer todayFile.Close()

// template.md 읽기
template, err := ioutil.ReadFile("template.md")
if err != nil {
fmt.Printf("Unable to read file: %v\n", err)
}

// 오늘 TIL에 쓰기
fmt.Fprintln(todayFile, string(template))

이렇게 합치면 오늘 파일을 생성하고 template.md를 읽고 오늘 파일에 쓰기를 실행할 수 있습니다.

ioutil은 deprecated

하지만 문제가 있습니다. ioutil은 deprecated 되어 있습니다. 이것을 어떻게 알게되었는가? 파일 읽기 활동도 OS 자원을 필요로 하기 때문에 닫는 행위도 필요하지 않는가? 라는 생각이 들었습니다.

For Go, ioutil.ReadAll / ioutil.ReadFile / ioutil.ReadDir deprecated

위에서 대안을 제시해주고 있습니다.

ioutil.ReadAll -> io.ReadAll
ioutil.ReadFile -> os.ReadFile
ioutil.ReadDir -> os.ReadDir
// others
ioutil.NopCloser -> io.NopCloser
ioutil.TempDir -> os.MkdirTemp
ioutil.TempFile -> os.CreateTemp
ioutil.WriteFile -> os.WriteFile
// template.md 읽기
template, err := os.ReadFile("template.md")
if err != nil {
fmt.Printf("Unable to read file: %v\n", err)
}

그래서 이렇게 바꿨습니다. 그리고 읽는 것에 파일을 닫아줄 필요는 없습니다. Close() 메서드가 template 변수에 없었습니다.

내일 출력하기

오늘을 time.Now()로 출력하면 됩니다. 문제는 일정한 간격을 두고 적용해야 합니다.

내일 TIL을 미리 만들어야 합니다. 오늘을 기준으로 내일 날짜를 표현해야 합니다.

markdown := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC).Format("060102")

이렇게 하니까 하루를 더할 수 있었습니다.

내일 TIL을 미리 만드는 이유는 내일을 계획할 때 유용하기 때문입니다.

부록: 0755는 무엇인가?

여전히 남는 의문이 있습니다. 0755는 무엇인가?

err := os.Mkdir(folder, 0755)
if err != nil {
fmt.Printf("Unable to write file: %v\n", err)
}

0755은 왜 2번째 인자로 대입해야 하는지 모르겠습니다. 대입이 필요한 이유와 값은 무슨 의미를 갖고 있는지 모르겠습니다.