iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
1
Software Development

Gosh!原來用 Go 寫一個 Unix Shell 這麼簡單系列 第 12

Day12-alias 指令別名(一)

前言

常常有人問我說用 command line 下指令的方式到底比用滑鼠操作 GUI(Graphical User Interface) 圖形介面 好在哪裡,那通常我會說用下指令的方式效率高很多,其中一個原因就是 alias

我可以把我常用的指令進行縮寫,譬如說在我的電腦上 gst 就是 git status 的意思,ga 則是 git add,用滑鼠開資料夾、按按鈕的時間已經夠我下很多個指令,效率自然高得多

因此我認為 alias 是 Shell 中必不可少的功能,接下來幾天就要實作他

alias 語法

在 Linux 中要建立一個 alias 可以使用 alias name='command -args',譬如說 alias gst='git status',這樣下 gs 就會得到 git status 的效果

如果要取消 alias 則是用 unalias name,這點跟 unset 環境變數很像,當然取消了之後就沒辦法再使用

目前問題

在真的開始實作之前必須先克服一個問題,大家還記得在 Day06-執行指令(二) 時我們寫了這些扣,當時為了把指令中的參數分開,以空白分割字串把 ps aux 切成 [ps, aux]

// 把使用者的輸入切割成 Array
// "ps aux" -> ["ps", "aux"]
args := strings.Split(input, " ")

但現在問題來了,因為 alias 的指令中也含有空白 ,譬如說 alias gst='git status',這樣引號中的指令就會被切開,變成 [alias, gst='git, status']

git status 不應該被切開,我們想要的是切成 [alias, gst='git status'],所以今天要來改造一下切割指令的部分

實作

演算法

針對這個問題我想到一個很不通用的爛方法(時間不多允許我偷懶一下XD),就是針對 alias 指令做特殊的切割,其他指令的話還是用舊的切割方式

  • 如果是 alias 開頭的指令

    若指令是 alias 開頭,那就只切割第一個 Space,譬如說 alias gst='git status' 就切成 [alias, 'git status']

  • 其他指令

    其他指令還是照舊,把所有的 Space 都切開,譬如說 ls -l -a -h 就切成 [ls, -l, -a, -h]

會用到的 function

  • strings.HasPrefix(s, prefix string)

    用來判斷某字串 s 是不是以字串 prefix 為開頭,等等會用來判斷使用者輸入的指令是不是 alias 開頭

  • strings.SplitN

    他跟之前介紹過的 strings.Split 很類似,Split 可以用來切開所有空白,而 SplitN 則是可以設定 最多切成幾段 ,超過他就不再切了,剛好適用於目前的情境

// paresArgs 就是上圖的 parseArgs
// 這邊要根據上面的演算法來實作他
func parseArgs(input string) []string {
    // 如果 input 是 "alias" 開頭,那最多就切成兩段
    // 也就是 ["alias", "ooo xxx ooo xxx"]
    // 後面的空白不會被切到
    if strings.HasPrefix(input, "alias") {
        return strings.SplitN(input, " ", 2)
    }
    
    // 如果不是 "alias" 開頭
    // 那就用原本的方法,把所有空白都切開
    // "ls -l -a" -> ["ls", "-l", "-a"]
    return strings.Split(input, " ")
}

executeInput 中把剛寫好的 parseArgs 拿來用

func executeInput(input string) error {
    // ...
    
    args := parseArgs(input)  // <---- HERE 

    if args[0] == "cd" {
        err := os.Chdir(args[1])
        return err
    }

    // ...
}

測試

因為今天只是實作 function 而不是完成了一個 feature,所以沒什麼好 Demo 的

為了檢驗剛剛寫的 parseArgs 有沒有正常運作,來寫個測試在 parseArgs_test.go

import "testing"

// 比較 a, b 兩個 Array 的內容是否一樣
func equal(a, b []string) bool {
    // 實作很簡單,有興趣到 Github commit 上看
}

func TestAbs(t *testing.T) {
    // 測試 "ls -l -a" 有沒有被正確切成 ["ls", "-l", "-a"]
    input := "ls -l -a"
    ans := []string{"ls", "-l", "-a"}

    // 如果 parse 出來的結果跟答案不一樣就是失敗
    if !equal(parseArgs(input), ans) {
        t.Fail()
    }

    // 第二組 input 跟答案
    // 測試有沒有被正確被切成 ["alias", "gst='git status'"]
    input = "alias gst='git status'"
    ans = []string{"alias", "gst='git status'"}
    
    // 如果 parse 出來的結果跟答案不一樣就是失敗
    if !equal(parseArgs(input), ans) {
        t.Fail()
    }
}

接著下 go test 指令,Go 就會自動執行所有 _test.go 結尾的檔案,包括剛剛寫的 parseArgs_test.go

如果錯誤的話就會跑出 Fail

小結

今天除了寫扣外還順便講了測試的部分,這種針對單一 function 的測試叫做 單元測試(Unit Test),如果是在一個團隊裡面開發的話,通常都會搭配簡單的 Unit Test 以防 腦殘 同事不小心把程式改壞XD,在部署前先用自動化測試檢查一遍大家也比較安心

今天寫的扣比較多一點,完整的實作放在這裡,有問題的話歡迎在下面提出來,沒問題的話就明天見囉

延伸閱讀


上一篇
Day11-存取環境變數(二)
下一篇
Day13-alias 指令別名(二)
系列文
Gosh!原來用 Go 寫一個 Unix Shell 這麼簡單30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言