從上一回的探索中,我已經大概知道怎麼自訂 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
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 是 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]
接下來繼續看 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
上述有一小段 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 副檔名
最後我來繼續研究一下可變參數的特性
假設我宣告了一個函數,可接收兩個固定參數,以及不固定數量的參數
以下的呼叫會怎麼對應呢?
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
以上是今天的挖礦,下一回我仍會繼續探索,電腦如何知道我的指令?
主題是指令碼的自動完成!