iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Software Development

在麥塊的農場裡寫 Lua系列 第 10

Day10 為什麼電腦懂我的指令?函數宣告 part2

從上一回的探索中,我已經大概知道怎麼自訂 CC: Tweaked 電腦開機跑的程式
也在過程中慢慢熟悉 Lua 的函數宣告語法
今天我想繼續探索其他的函數宣告特性
主題則是,為何 CC: Tweaked 電腦懂我的指令?

請大家回想
當我打 help 的時候,電腦知道要執行 /rom/programs/help.lua
當我打 hello 的時候,電腦也知道要執行 /rom/programs/fun/hello.lua
它是怎麼知道的?!

首先我回頭看 bios.lua 執行 shell.lua 之後
shell.lua 會先跑 startup.lua
接著就開始一段無窮迴圈在 shell.lua #596

while not bExit do
    -- 略 --
    local sLine
    if settings.get("shell.autocomplete") then
        sLine = read(nil, tCommandHistory, shell.complete)
    else
        sLine = read(nil, tCommandHistory)
    end
    -- 略 --
    shell.run(sLine)
end

read 是讀取玩家的輸入,這邊看起來很複雜,我先不深入
先來看 shell.run

定義 Lua 的可變參數函數

function shell.run(...)
    local tWords = tokenise(...)
    local sCommand = tWords[1]
    if sCommand then
        return shell.execute(sCommand, table.unpack(tWords, 2))
    end
    return false
end

如上述範例,宣告函數時給三個點就表示它可接收不定數量的參數值
而如果要原封不動地將這些參數,全部傳入其他函數,也是相同的表示
tokenise() 是將使用者輸入的整行文字切割,接著只取第一個 token 當作指令
並繼續傳給 shell.execute()

補充:table.unpack()

table.unpack 是 Lua 為 table 型別提供的基礎函數
可以比較方便地將 table 內的部分元素取出並回傳
這邊 table.unpack(tWords, 2) 相當於
tWords[2], tWords[3], tWords[4] ....... 一直到最後一個數值
也就是將使用者輸入的參數取出,並傳入 shell.execute()
如果 table.unpack 有給第三個參數,例如

table.unpack(tWords, 2, 5)

那麼就相當於
tWords[2], tWords[3], tWords[4], tWords[5]

定義 Lua 的可變參數函數,但部分參數固定

接下來繼續看 shell.execute
它只有 command 是固定參數,其餘都是變動參數,因為每個指令可接收的參數量是不同的
所以這裡的 ... 代表該指令的所有參數
例如假設使用者輸入

set motd.path "/mymotd.txt"

那麼 command = 'set', 後面的參數則會形成一個 table { "motd.path", "/mymotd.txt" }

function shell.execute(command, ...)
    expect(1, command, "string")
    for i = 1, select('#', ...) do
        expect(i + 1, select(i, ...), "string")
    end

    local sPath = shell.resolveProgram(command)
    if sPath ~= nil then
        -- 略 --
        local sDir = fs.getDir(sPath)
        local env = createShellEnv(sDir)
        env.arg = { [0] = command, ... }
        local result = os.run(env, sPath, ...)

        -- 略 --
        return result
       else
        printError("No such program")
        return false
    end
end

補充:Lua 的基礎函數 select

上述有一小段 select 語法,這是 Lua 內建的函數之一,可以從 table 取得特定 index 的元素
例如這是取得第 3 個元素

print(select(3, ...))

而如果是給 #,則是取得 table 內元素的總個數

print(select('#', ...))

這相當於

args = { ... }
print(#args)

這在 shell.lua #575 也有用到這樣的雨法

local tArgs = { ... }
if #tArgs > 0 then
    -- "shell x y z"
    -- Run the program specified on the commandline
    shell.run(...)

解析使用者輸入的指令位置

接下來又到了上次提到的 shell.resolveProgram()

function shell.resolveProgram(command)
    expect(1, command, "string")
    -- Substitute aliases firsts
    if tAliases[command] ~= nil then
        command = tAliases[command]
    end

    -- If the path is a global path, use it directly
    if command:find("/") or command:find("\\") then
        local sPath = shell.resolve(command)
        if fs.exists(sPath) and not fs.isDir(sPath) then
            return sPath
        else
            local sPathLua = pathWithExtension(sPath, "lua")
            if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
                return sPathLua
            end
        end
        return nil
    end

     -- Otherwise, look on the path variable
    for sPath in string.gmatch(sPath, "[^:]+") do
        sPath = fs.combine(shell.resolve(sPath), command)
        if fs.exists(sPath) and not fs.isDir(sPath) then
            return sPath
        else
            local sPathLua = pathWithExtension(sPath, "lua")
            if fs.exists(sPathLua) and not fs.isDir(sPathLua) then
                return sPathLua
            end
        end
    end

    -- Not found
    return nil
end

第二段的部分,註解已經說明了大概
若使用者輸入的指令含有 / 或
則直接在根目錄尋找指令
-- If the path is a global path, use it directly

第三段則是當使用者輸入一般指令,就會去取 Path 環境變數
Path 的設定也是在 shell.lua

local sPath = parentShell and parentShell.path() or ".:/rom/programs"

我想可以先忽略 parentShell,應該可以認定 sPath 的預設值就是 .:/rom/programs
也就是說,當使用者輸入任何指令的時候,並且沒有給 / 或
那麼就會從當下的位置尋找 lua 程式
如果找不到,就會去 /rom/programs 的位置尋找
而 pathWithExtension 很顯然則是自動幫我們加上 .lua 副檔名

Lua 的可變參數函數如何對應傳入值

最後我來繼續研究一下可變參數的特性
假設我宣告了一個函數,可接收兩個固定參數,以及不固定數量的參數
以下的呼叫會怎麼對應呢?

function func(a, b, ...)
    args = { ... }
    print(a, b, args[1], args[2], #args)
end

func(1)             -- 1 nil nil nil 0
func(1, 2)          -- 1  2  nil nil 0
func(1, 2, 3)       -- 1  2   3  nil 1 
func(1, 2, 3, 4)    -- 1  2   3   4  2

也就是說,會先對應固定參數,如果傳入參數不夠,則會給 nil
其餘的參數則全都會形成 table

以上是今天的挖礦,下一回我仍會繼續探索,電腦如何知道我的指令?
主題是指令碼的自動完成!


上一篇
Day9 自訂開機執行的程式碼 - 函數宣告與語法糖
下一篇
Day11 為什麼電腦能自動完成指令 - Lua 的多值回傳
系列文
在麥塊的農場裡寫 Lua30

尚未有邦友留言

立即登入留言