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

以上是今天的挖矿,下一回我仍会继续探索,电脑如何知道我的指令?
主题是指令码的自动完成!


<<:  Day-13 Miku Memory

>>:  Day 13 - 函式烤肉

Python 列印

我们终於进到写程序的部分了,前几天我们都在教学基本的使用,今天就让我来教大家吧!我们就先开到程序档的...

Day 23 | 使用ManoMotion制作打地鼠游戏Part1 - 手部侦测及地鼠设定

在上一篇文章介绍了ManoMotion的安装与介绍,今天我们要使用ManoMotion来制作打地鼠游...

建立第一个RESTful api server(实作篇)-1(Day12)

前面介绍了那麽多内容,那接下来就让我们来实作第一个restful api server吧 在每个後端...

Vue 如何在 LocalHost 开发环境时使用 HTTPS

如果你有 Localhost 开发环境需要以 HTTPS 浏览时,可以参考以下方法: 方法一:vue...

EP 4: Use Fonts to design Icon in TopStore App

Hello, 各位 iT邦帮忙 的粉丝们大家好~~~ 本篇是 Re: 从零开始用 Xamarin 技...