OpenResty OpsLang™ 使用者手冊

名稱

Ops Language (運維語言)使用者手冊

目錄

描述

這本手冊是使用者角度的 Ops 語言的文件。

本文件中的某些特性可能在當前 Ops 語言版本還沒有實現。不過 Ops 語言開發團隊正在快速的跟進。 我們會盡量把未實現的特性包含在文件裡。如果有疑問,請直接與 OpenResty Inc. 公司聯絡。

本文件仍然是一份草案。許多細節依舊會面臨更新。

習語

為了表達方便,本文中用 opslang 表示 Ops 語言。

有問題的樣例程式碼會在每行開頭字首一個問號,?。比如:

? true =>
?    say($a = 3);

程式佈局

在最高階別,一個 Ops 程式由一個主程式檔案和一些可選的模組檔案集合組成。

每個 .ops 檔案都包含下面型別的各種宣告語句:

  1. 模組宣告語句,
  2. 目標預設目標 宣告,
  3. 頂級 變數 宣告,
  4. 例外宣告,和
  5. 使用者動作宣告。

每個 Ops 模組檔案的開頭都必須出現第一個語句,而非模組語句不能有這個語句。

Ops 主程式檔案必須包含至少一個全域性宣告。

其它東西都是可選的。

為了與其它程式語言的傳統一直,Ops 語言的 “hello world” 程式看上去像下面這樣:

goal hello {
    run {
        say("Hello, world!");
    }
}

假設這個程式在檔案 hello.ops 裡,我們可以用下面這樣的一條命令執行之:

$ opslang --run hello.ops
Hello, world!

或者用 opslang 當一個編譯器,生成名為 hello.lua 的 Lua 模組:

opslang -o hello.lua hello.ops

然後執行用 OpenResty 命令列工具 resty 執行這個生成的 Lua 模組:

$ resty -e 'require "hello".main(arg)'
Hello, world!

回到目錄

集腋成裘

識別符號

opslang 裡面的識別符號是一個或多個劃線連線的是由字母字元和下劃線組成的序列。不過,字的開頭不能是下劃線。 下面是一些有效的 opslang 識別符號的例子:

foo
hisName
uri-prefix
Your_Name1234
a-b_1-c

Ops 語言是一種大小寫敏感的語言。所以像 fooFoo 這兩個識別符號是完全不同的東西。

除了變數名之外,識別符號不能是語言的關鍵字。

回到目錄

關鍵字

Ops 有下列關鍵字:

for     while      if      while     func      action
ge      gt         le      lt        eq        ne
contains           contains-word     suffix    prefix
my      our        macro   use       INIT      END
as      rx         wc      qw        phase     goal
exception          return  label     for       async
lua

回到目錄

全稱

使用者可以使用全稱來引用其它 Ops 模組的符號,或者引用一個目標裡的特定動作階段。

比如,std.say 就是明確引用 std 模組(或名字空間)中的函式 say()。 而 all.run 則是引用目標 all 裡面的動作階段的 run 動作。

使用者也可以結合這兩個例子。比如 foo.all.run 是模組 foo 裡面目標 allrun 動作階段。

回到目錄

變數

變數名包含兩個部分:一個前置的特殊字元,叫印記,後面跟著識別符號。印記用於標註變數雷習慣。 本語言支援下列印記:

  • $ 用於 標量變數
  • @ 用於 陣列變數
  • % 用於 雜湊變數

標量變數儲存簡單數值,比如數字、字串、布林和數量等。

陣列變數是一個包含簡單變數的有序列表。

雜湊變數是一個無序的鍵值對列表。

變數通常像下面這樣用 my 關鍵字來宣告:

my Int $count;
my Str $name;
my Str @domains;
my Bool %map{Str};

變數宣告也可以帶一個初始值,例如:

my Int $count = 0;
my Str @domains = qw/ foo.com bar.blah.org /;

在任意範圍裡定義的每個標量變數,在其生命期裡頭都只能有一個資料型別。每個變數的資料型別都必須在編譯時是可確定的。 標量變數有 4 種資料型別:

  • Str
  • Num
  • Int
  • Bool

不過,Ops 語言在其它環境裡,不支援其它資料型別,比如 ExceptionNil

使用者必須為用 my 定義的或是定義成使用者動作引數的每個標量明確宣告一個資料型別。

為了方便,同樣資料型別的多個變數可以用一條 my 語句宣告,像下面這樣:

my Str ($foo, $bar, $baz);

甚至可以混合標量、陣列和雜湊變數:

my Str ($foo, @bar, %baz{Int});

不過這樣的變數宣告不能接受任何初始化表示式。

回到目錄

陣列變數

陣列變數儲存一個線性簡單值的列表,稱作陣列。

下面是一個例子:

goal all {
    run {
        my Int @ints;

        push(@ints, 32),
        push(@ints, -3),
        push(@ints, 0),
        say("index 0: ", @ints[0]),
        say("index 2: ", @ints[2]),
        say("array: [@ints]");
    }
}

如上所示,陣列的元素型別是在 my 關鍵字和 @ints 變數名之間宣告的。

標準函式 push 給陣列末尾追加新的元素。多個元素可以用一次 push() 呼叫同時追加。

之後可以用 @arr[index] 這樣的寫法訪問獨立的元素。這個記法也可以用於給特定元素設定新值,如:

@ints[2] = 100

像標量一樣,完整的陣列也可以插入到字串文字中。

執行這個 int-array.ops 例子程式生成:

$ opslang --run int-array.ops
index 0: 32
index 2: 0
array: [32 -3 0]

還有其它一些標準函式可以運算元組:

  • pop() 函式返回並刪除陣列的最後一個元素;
  • shift() 函式返回並刪除陣列的第一個元素;
  • unshift 函式在陣列開頭前置一個或多個元素;
  • elems 函式返回陣列長度。

如果在布林環境中使用,在陣列變數不為空的時候,他們得出真值結果。比如:

goal all {
    run {
        my Num @nums;

        {
            @nums =>
                say("array is NOT empty"),
                done;

            true =>
                say("array is empty");
        };
    }
}

執行這個 array-cond.ops 例子得出:

$ opslang --run array-cond.ops
array is empty

我們可以用 字面陣列 初始化一個陣列,像下面這樣:

goal all {
    run {
        my Num @nums = (3.14, 0);

        {
            @nums =>
                say("array is NOT empty"),
                done;

            true =>
                say("array is empty");
        };
    }
}

現在執行這個例子會給出輸出 array is NOT empty,因為陣列現在有兩個元素。

陣列變數可以用 for 語句 遍歷。

回到目錄

雜湊變數

哈西變數儲存一個字典資料,這個字典資料是從鍵字到簡單變數的對映。

下面是一個簡單例子:

goal all {
    run {
        my Num %ages{Str} = (dog: 1.5, cat: 5.4, bird: 0.3);

        say("bird: ", %ages<bird>),
        say("dog: ", %ages{'dog'}),
        say("cat: ", %ages{"cat"}),
        say("tiger: ", %ages<tiger>);
    }
}

執行這個 ages.ops 例子程式得出:

$ opslang --run ages.ops
bird: 0.3
dog: 1.5
cat: 5.4
tiger: nil

這裡我們先定義了一個叫 %ages 的雜湊變數(注意 % 印記)。它的鍵字型別是 Str 而其值型別是 Num。 同時我們用 字面雜湊值 給這個雜湊變數的 3 種動物 dogcatbird 初始化了年齡。 然後我們輸出這個雜湊變數各個鍵字的值。請注意在這個雜湊變數裡頭並不存在 tiger 鍵字,所以我們在輸出它的時候得到了個 nil 值。

如例子所示,如果字串鍵字是一個字,我們可以用 %hash<word> 語法做為更通用形式%hash{'word'}%hash{"word"} 的縮寫。一般而言,要讀取一個指定鍵字的值,我們可以用 %hash{key};如果要覆蓋一個指定鍵字的值,我們可以 %hash{key} = value

我們也可以在鍵字裡頭使用標量變數,如:

my $key = 'dog';

say("$key: ", %ages{$key});

還支援其它型別的鍵字,比如:

goal all {
    run {
        my Str %weekdays{Int} = (1: "Monday", 2: "Tuesday", 3: "Wednesday",
                                 4: "Thursday", 5: "Friday");

        say(%weekdays{3}),
        say(%weekdays{5});
    }
}

執行例程 weekdays.ops 得到:

$ opslang --run weekdays.ops
Wednesday
Friday

如果在布林環境中使用,那麼雜湊變數非空時返回真,否則返回假。比如:

goal all {
    run {
        my Int %ages{Str};

        {
            %ages =>
                say("hash NOT empty"),
                done;

            true =>
                say("hash empty");
        };
    }
}

執行例程 hash-empty.ops 得出:

$ opslang --run hash-empty.ops
hash empty

雜湊變數無法在 雙引號字串單引號字串 中做值替換。在這些環境裡, % 自負總是解析成字面意思。

使用者可以使用 for 語句遍歷雜湊變數。

回到目錄

捕獲變數

本語言提供特殊的捕獲變數用於儲存前面的正則匹配中抓到的子匹配的內容(如果有的話)。比如,變數 $1 儲存捕獲組中的第一個子匹配, 而 $2 儲存第二個匹配組,$3 對第三個,如此類推。特殊變數 $0 儲存完整正則匹配的整個子淄川,不管具體哪個子匹配抓到的分組。

讓我們看看下面的例子:

goal all {
    run {
        my Str $s = "hello, world";

        {
            $s contains /(\w+), (\w+)/ =>
                say("0: [$0], 1: [$1], 2: [$2]"),
                done;

            true =>
                say("not matched!");
        };
    }
}

執行此 Ops 程式輸出:

0: [hello, world], 1: [hello], 2: [world]

回到目錄

函式名與函式呼叫

本語言廣泛使用函式於各種謂語和規則中的動作。 如果需要,使用者也可以定義自己的函式(像 使用者動作裡那樣)。

標準函式和使用者定義動作可以用完全相同的方式使用。 實際上,許多 opslang 編譯器搭載的標準函式實際上是在 std 模組裡以 “使用者動作” 的方式實現的。

函式名是用識別符號直接表示的,不需要印記。

函式呼叫是用函式名後頭跟著一對兒圓括弧,裡頭放上引數來呼叫的,像下頭這樣:

say("hello, ", "world")

不帶引數的函式呼叫可以省略圓括弧。比如:

redo()

可以簡化為:

redo

引數可以用位置方式或者名字的方式傳遞。比如,內建的 say() 動作函式,接受位置引數作為返回訊息體的片段,如前面例子演示的那樣。 命名引數用引數名和一個冒號字元當字首傳入,像下面這樣:

goto(my-label, tries: 3);

這個 goto() 呼叫接受一個叫 tries 的命名引數,值是 3

內建函式可能需要特定引數以命名引數方式傳遞,而其它的以位置引數傳遞。 請參考對應的內建函式的文件獲取實際用法的說明。

還有一些語法構造等效於函式呼叫。 比如 shell 包圍字串美元符動作直接用作一個動作。

回到目錄

字面文字串

單引號包圍字串

單引號包圍字串是字面字串值包圍在單引號 ('') 裡頭,像下面這樣:

'foo 1234'

字元 $@ 總是他們字面的意思。標量和陣列在裡頭絕不會被代入實際值。

單引號包圍字串只支援下面的逃逸序列:

\'
\\

單引號包圍字串字面裡頭出現的任何其它 \ 字元都會被理解為一個字面的 \

回到目錄

雙引號包圍字串

雙引號包圍字串是一個字面串被包圍在雙引號("")裡頭,如下所示:

"hello, world!\n"

雙引號包圍字串裡頭支援下面的逃逸序列:

\a
\b
\f
\n
\r
\t
\v
\\
\0
\$
\@
\%
\'
\"

雙引號包圍字串裡頭可以對標量和陣列變數進行變數代換,如下所示:

"Hello, $name!"

"Names: @names"

如果在變數名後面有容易引發歧義的字元出現,我們可以用花括弧包圍變數來消歧,如下所示:

"Hello, ${name}ism"

"Hello, @{names}ya"

為使用方便,本語言還支援另外一種變數代換語法(類似 shell 包圍字串):

"Hello, $.name!"

"Hello, @.names!"

或者是花括弧形式:

"Hello, $.{name}!"

"Hello, @.{names}!"

因此在雙引號包圍字串裡頭字面的 $@ 字元必須用 \ 逃逸,以避免意外的變數代換 ,如:

"Hello, \$name!"

回到目錄

方括弧包圍長文字

方括弧包圍長文字是 Lua 風格的長方括弧包圍的字面字串。 長方括弧的例子有 [[...]][=[...]=][==[...]==] 等等。

長方括弧內部的所有字元都呈現其字面含義,在這裡完全沒有任何特殊字元。比如,字面串:

[=[\n"hello, 'world'!"\]=]

等效於 單引號包圍字串 '\n"hello, \'world\'!"\\'

為了簡化使用,如果長方括弧內部的第一個字元是換行符,那麼這個換行符會被自動刪除,所以下面的長方括弧字串:

[=[
hello world
]=]

等效於 雙引號包圍字串 "hello world\n"

回到目錄

Shell 包圍字串

Shell 包圍字串提供了一種在 Ops 程式裡頭嵌入 shell 命令字串的便利的記法。它接受下面各種形式:

sh/.../

sh{...}

sh[...]

sh(...)

sh'...'

sh"..."

sh!...!

sh#...#

類似長方括弧包圍字串,所有 shell 包圍字串裡頭的字元都是字面含義。 我們可以用雙引號逃逸引號字元,但是逃逸序列自身仍然是最後代換的字串值的一部分。

Ops 編譯器總是把已分析過的 shell 包圍的字串當作 Bash 命令看到。所以不要在這個語法構造裡面使用隨意的字串。

shell 包圍的字串裡頭不支援形如 $foo@foo 的變數代換,因為 Bash 語言自己使用這種記法。 所以我們支援在 shell 引號內部使用另外一種“點格式”的變數代換,如下所示:

sh/echo "$.name!"/

或者使用化括弧消歧:

sh/echo "$.{name}ism"/

為了通用,這種“點格式”的變數代換語法也被 雙引號包圍字串正則字面 所支援。

和其它做變數代換的環境不同的是,shell 引號還會根據變數所使用的 shell 環境,對代換的 Ops 變數做恰當的 shell 字串的引號包圍和逃逸。 比如,在 Bash 的雙引號內外使用的變數會根據 Bash 語法進行不同的引號包圍和逃逸處理。假如我們要代換下面的變數:

$name = "hello\n\"boy\"!";

它接受下面的字面值:

hello
"boy"!

在我們代換他到 shell 的雙引號包圍裡的時候,

sh/echo "$.name"/

我們會拿到下面的 shell 命令串,並最後送到終端模擬器裡:

echo "hello
\"boy\"!"

(請注意 ! 並不需要逃逸,因為標準函式 setup-sh() 會禁用 Bash 的歷史命令列表功能。)

另一方面,如果在任何 shell 的雙引號外部代換 $name 變數,那麼它的逃逸是不同的:

sh/echo $.name/

生成最後的 shell 命令串是:

echo hello\
\"boy\"\!

關於在 shell 雙引號內外使用代換變數一個需要注意的重要的問題是,後者(shell 雙引號外部)不會逃逸空白字元, 因此標量變數很可能最後變成好幾個 shell 值,如:

my Str $files = 'a.txt b.txt';

sh{ls $.files},
stream {
    found-prompt => break;
}

這個例子等效於:

sh{ls a.txt b.txt},
stream {
    found-prompt => break;
}

基本上, $files 變數值會被解析成兩個獨立的引數遞給 shell 命令 ls。 因此,如果,如果檔案路徑可能包含空白,我們一定總是用雙引號包圍需要代換的 Ops 變數,如下所示:

my Str $file = '/home/agentzh/My Documents/a.txt';

sh{ls "$.file"},
stream {
    found-prompt => break;
}

如果本例中沒有雙引號,那麼 ls 命令會嘗試尋找兩個獨立的檔案 /home/agentzh/MyDocuments/a.txt

shell 引號包圍也支援其它 shell 變數環境,比如 $(...), 反勾號 (`...`),$((...))$"..."$'...' 等等。

在 shell 的單引號內是不進行變數代換的,這點跟 shell 語言自身一樣。如:

sh/echo '$.foo'/

會生成最後的 shell 命令串 echo '$.foo'

使用者總可以把最後生成的 shell 引號包圍字串丟給 print()say() 函式輸出出來檢視,像下面這樣:

say(sh/echo $.foo/)

使用者應該在 shell 引號裡面避免使用控制字元,比如 tabs 和哪些不可列印的字元(比如 ESC 字元)。

如果 shell 包圍的字串直接在動作裡使用,像下面這樣:

true =>
    sh/echo hi/,
    done;

那麼它會自動被轉換成一個 std.send-cmd() 函式呼叫,把這個 shell 引號包圍的字串當作引數傳遞進去,如下所示:

true =>
    std.send-cmd(sh/echo hi/),
    done;

陣列變數也可以在這個環境裡代換進去,就像 雙引號包圍字串一樣。比如:

my Str @foo = qw(hello world);

goal all {
    run {
        say(sh/echo @.foo "@.foo"/);
    }
}

執行這個例子生成:

echo hello world "hello world"

回到目錄

美元符動作

美元符動作是一種節約 Ops 程式設計師敲鍵的便利語法糖。一個美元符動作 $ CMD 實際上是下面 Ops 程式碼片段的縮寫:

std.send-cmd(sh/CMD/),  # 實際引號包圍的字元是啥無所謂
stream {
    found-prompt => break;
}

上面的引號字元 / 是隨意的。使用者可以在美元符動作裡隨意使用斜槓(除號)。

美元符和空白後面的 CMD 是一個使用者 shell 命令字串,如 shell 包圍字串 裡頭的那樣。 這個命令列字傳一直延伸到行尾,不包括行尾的任何逗號或者分號字元。

感謝隱含的 stream { found-prompt => break; } 塊,一個美元符動作在 shell 命令結束執行之前(也就是出現一個新的 shell 提示符之前)不會結束。

如果在一個動作鏈裡頭使用美元符動作(像在一個 規則 的後繼部分),仍然需要逗號分隔符,就像下面:

true =>
    $ cd /tmp/,
    $ mkdir foo,
    done;

確保自己不要在逗號和換行符之間插入任何空白自負。如果美元符動作處在動作鏈的末尾, 也要求程式碼上在換行符之前加一個分號,像下面這樣:

true =>
    $ cd /tmp;

true =>
    say("...");

重要提示:我們仍然可以覆蓋美元符動作隱含引入的預設 stream {} 塊。方法是我們緊跟在美元符動作後面定義一個明確的 stream {} 塊,如下所示:

$ echo hi,
stream {
    out contains-word /hi/ =>
        say("hit!");

    found-prompt =>
        break;
}

一定要記得在自己的stream {} 塊結尾新增 found-prompt 規則, 否則你的美元符動作將不會等待 shell 提示符出現,導致超時等等錯誤。

回到目錄

數值常量

數值常量可以用下列形式之一書寫:

1527
3.14159
-32
-3.14
78e-3      (帶十進位制指數)
0xBEFF     (十六進位制)
0157       (八進位制)

回到目錄

正則字面值

正則字面值用於宣告一個相容 Perl 的正規表示式值。 它是透過關鍵字 rx 和一個引號包圍結構來表示的。下面是一些例子:

rx{ hello, \w+}
rx/ [0-9]+ /
rx( [a-zA-Z]* )
rx[ [a-zA-Z]* ]
rx" \d+ - \d+ "
rx' ([^a-z][a-z]+) '
rx! ([^a-z][a-z]+) !
rx# ([^a-z][a-z]+) #

使用者可以在正則字面值裡頭自由使用化括弧、斜槓、圓括弧、雙引號或者單引號。 他們都是一樣的,區別只是在正則字串裡頭需要逃逸的引號字元是啥。比如在 rx(...) 形式裡頭, 正則字串裡頭的斜槓字元(/)是不需要任何逃逸的。 預設時,在正則值裡頭使用空白字元是含義的,只有在字元表構造裡才有含義(比如,[a-z])。 這樣可以鼓勵使用者把自己的正則字傳結構化得更易讀一些。

在正則裡頭可以宣告很多選項,比如:

rx:i/hello/

要求一個大小寫無關的模式 hello 的匹配。類似的:

rx:x/ hello, world /

令模式字串裡頭使用的空白字元無意義,因此這個正則匹配字串 "hello,world",但是無法匹配 "hello, world"

可以同時宣告多個選項,把他們疊在一起就行:

rx:i:s/hello, world/

如果不宣告選項,我們可以去掉字首 rx,只用斜槓來表示一個正則字面值,如:

/\w+/

/hello world/

Ops 的正則字面裡頭,元字元 . 總是匹配任何字元,包括新航字元("\n")和特殊模式 \s 也總是匹配任何空白字元,包括新行。

回到目錄

萬用字元字面值

萬用字元字面值是指涉谷摩納哥一個字串,匹配 UNIX 風格的萬用字元語法的模式。 它是用一個關鍵字 wc 帶著一個引號包圍結構表示的,比如:

wc{foo???}
wc/*.foo.com/;
wc(/country/??/);
wc"[a-z].bar.org"
wc'[a-z].*.gov'

跟正則字面一樣,萬用字元字面也可以使用靈活的引號字元。

支援三種萬用字元元模式: * 用於匹配任何子字串, ? 用於匹配任何單字元,[...] 匹配字元表。

萬用字元可以宣告一個或多個選項,比如:

wc:i/hello/

要求對模式 hello 的大小寫無關的匹配。

回到目錄

字面陣列

字面陣列提供了宣告常量陣列值的一個簡便方法。 其通用語法如下:

(elem1, elem2, elem3, ...)

下面是個例子:

action print-int-array(Int @arr) {
    print("[@arr]");
}

goal all {
    run {
        print-int-array((3, 79, -51));
    }
}

執行這個 print-int-array.ops 例子生成:

$ opslang --run print-int-array.ops
[3 79 -51]

字面陣列也可以使用者初始化一個 陣列變數或者給一個接收陣列型別的使用者動作設定一個預設值。

出於方便,允許在最後一個元素後面多一個逗號,像下面這樣:

(3,14, 5, 0,)

回到目錄

引號包圍字

引號包圍字提供一個簡便的、不用連續敲入太多引號的、宣告一堆常量字面字串陣列的方法。

它用關鍵字 qw 後面跟著一個靈活的引號包圍結構標註。比如:

qw/ foo bar baz /

等效於:

("foo", "bar", "baz")

和正則字面以及萬用字元字面一樣,使用者可以選擇各種引號包圍字元用於引號包圍結構,比如:

qw{...}
qw[...]
qw(...)
qw'...'
qw"..."
qw!...!
qw#...#

回到目錄

字面雜湊值

字面雜湊值提供宣告常量雜湊值的簡便方法。一般的語法如下:

(key1: value1, key2: value2, key3: value3, ...)

下面是一個例子:

action print-dog-age (Int %ages{Str}) {
    say("dog age: ", %ages<dog>);
}

goal all {
    run {
        print-dog-age((dog: 3, cat: 4));
    }
}

執行這個 print-dog-age.ops 例子輸出為:

$ opslang --run print-dog-age.ops
dog age: 3

字面雜湊值也可以用於初始化 雜湊變數 或者給一個接收雜湊型別引數的使用者動作設定預設引數。

為了簡便,允許在最後一個鍵值對背後多一個逗號。

(dog: 3, cat: 5,)

回到目錄

布林

布林值呈現為內建函式呼叫 true()false() 的值。 所有關係運算也計算出布林值。

下面數值被認為是“條件為假”:

  • 數值 0
  • 字串 “0”
  • false() 的值
  • 空字串
  • 空陣列
  • 空雜湊表

所有其它值都被認為是“條件為真”。

函式呼叫 true()false() 經常被簡寫為 truefalse

回到目錄

註釋

註釋以字元 # 開頭,並且延續到當前行尾。比如:

# 這是一個註釋

也支援塊註釋,如:

#`(
This is a block
comment...
)

請注意 # 後面緊跟的反勾號和左緣括弧。塊註釋內部的圓括弧,只要是成對的,就可以繼續使用, 巢狀也行:

#`(
3 * (2 - (5 - 3))
)

回到目錄

例外

Ops 支援高效和強大的例外模型,類似使用者在其它程式語言,比如 C++ 和 Java 看到的那樣。

有兩種例外型別,標準例外和使用者定義例外。 標準例外如下(每種例外有一句簡要描述):

  • timeout

    讀超時或者等待終端模擬器(tty)超時。

  • failed

    前面的(shell)命令帶著非 0 返回碼返回。

  • found-prompt

    前面的(shell)命令結束(不管返回碼是多少)。請注意這個例外不會被丟擲,因為它不是致命例外。

  • error

    讀寫終端模擬器(tty)的時候發生了一些奇怪的錯誤,但並非 timeoutclosed 例外。

  • closed

    終端模擬器(tty)的連線提前關閉。

  • too-much-out

    緩衝了太多從終端模擬器(tty)中讀取的資料。這個限制可以透過 opslang 的命令列引數 --max-out-buf-size SIZE 設定。

  • too-many-tries

    gotoredo 或其它重試機制嘗試了太多次。

如果要引用一個例外,只要以例外名作為函式名呼叫這個函式即可。

如果在一個規則條件裡頭引用例外,那麼這就是 Ops 語言的捕獲前面動作中生成的例外的方法。 下面是例子:

goal all {
    run {
        $ ( exit 3 ),
        {
            failed =>
                say("cmd failed with exit code: ", exit-code);
        },
        say("done");
    }
}

執行這個 Ops 程式生成下面輸出(假設程式檔名是 failed-cmd.ops):

cmd failed with exit code: 3
done

本例中的美元符動作 $ ( exit 3 ) 一開始為當前 Bash 命令返回退出碼 3, 因此導致丟擲標準的 failed 例外。在隨後的動作塊理,以 failed 為(唯一)條件的的規則捕獲這個 failed 例外, 並且列印出前面 shell 命令對應的退出碼。因為 failed 例外被合理捕獲和處理,所以執行流繼續正常執行到最後的 say("done") 動作然後生成輸出。

如果我們把前面例子中不互毆例外的動作快去掉,像下面這樣:

goal all {
    run {
        $ ( exit 3 ),
        say("done");
    }
}

那麼這個 Ops 程式就會因未捕獲的 failed 例外,在執行美元符動作之後立即退出,像下面:

ERROR: failed-cmd-uncaught.ops:3: failed: process returned 3
  in rule/action at failed-cmd-uncaught.ops line 3

failed 例外也回自動捕獲錯誤資訊,如果有的話,像下面這樣:

goal all {
    run {
        $ ( echo "Bad bad bad!" > /dev/stderr && exit 3 ),
        say("done");
    }
}

生成

ERROR: failed-msg.ops:3: failed: process returned 3: Bad bad bad!
  in rule/action at failed-msg.ops line 3

如果在標準函式 throw 中使用一個例外當引數,那麼 throw() 函式呼叫會向當前 Ops 程式上層丟擲一個例外。 如果上層都不捕獲這個例外,那麼這個 Ops 程式會帶著一條錯誤資訊退出。

回到目錄

使用者例外

Ops 程式設計師可以重用上面說的任何標準例外,或者定義自己都例外。 要定義一個新例外,只要像下面這樣,在任何 .ops 原始檔頂級範圍內使用 exception 語句宣告即可:

exception too-large;

這裡我們定義了一個新的名為 too-large 的例外。

多個例外可以在同一個 exception 語句裡宣告,像下面:

exception foo, bar, baz;

我們可以用高標準函式 throw,在我們 Ops 程式裡頭隨時丟擲這樣的例外。 使用者例外可以像標準例外一樣捕獲,只要在規則條件裡,把對應例外名當作函式呼叫的方式來飲用即可。比如,

exception too-large;

action foo (Int $n) {
    {
        $n > 100 =>
            throw(too-large);
    };
}

goal all {
    run {
        foo(101),
        {
            too-large =>
                say("caught too large"),
                done;
        },
        say("done");
    }
}

執行此程式生成:

caught too large
done

類似的,未捕獲的使用者例外導致 Ops 程式退出,跟標準例外一樣。 讓我們看看下面例子:

exception too-large;

action foo (Int $n) {
    {
        $n > 100 =>
            throw(too-large);
    };
}

goal all {
    run {
        foo(101),
        say("done");
    }
}

它生成輸出:

ERROR: too-large.ops:6: too-large
  in action 'foo' at too-large.ops line 6
  in rule/action at too-large.ops line 12
  in rule/action at too-large.ops line 12

throw() 裡也可以申明一條文字資訊,如:

exception too-large;

action foo (Int $n) {
    {
        $n > 100 =>
            throw(too-large, msg: "$n is larger than 100");
    };
}

goal all {
    run {
        foo(101),
        say("done");
    }
}

生成

ERROR: throw-msg.ops:6: too-large: 101 is larger than 100
  in action 'foo' at throw-msg.ops line 6
  in rule/action at throw-msg.ops line 12
  in rule/action at throw-msg.ops line 12

現在可以看到第一行裡頭有更詳細的錯誤資訊。

模組裡定義的例外總是可以用形如 <module-name>.<exception-name> 的全稱引用。

回到目錄

標籤和 Goto

Ops 語言提供 goto 函式,類似於 C 語言裡的 “goto” 語句,但是安全很多。

函式 goto() 可以用於直接跳轉到當前範圍內外的一個指定名稱到標籤動作。

但是,不能跨當前使用者動作或者當前全域性動作階段進行跳躍。

可以跳轉過去的動作必須帶一個使用者定義標籤,如下所示:

goal all {
    run {
        say("before"),

    again:

        say("middle"),
        goto(again),
        say("after");
    }
}

在這裡我們給動作 say("middle") 賦予裡一個叫 “again” 的標籤,然後以標籤為唯一引數呼叫 goto 函式。請注意我們把 again 標籤當一個函式呼叫傳遞。 呼叫goto(again) 等效於 goto(again())again() 函式呼叫計算出一個等於前面給 again 標籤定義的標籤的型別值。

我們可以執行這個例子,看看輸出甚麼(假設這個程式在檔案 goto.ops 裡):

$ opslang --run goto.ops
before
middle
middle
middle
middle
ERROR: goto.ops:8: too-many-tries: attempted to do goto for 4 times
  in rule/action at goto.ops line 8

這個程式因為丟擲例外 而異常退出:

ERROR: goto.ops:8: too-many-tries: attempted to do goto for 4 times

這是因為預設時 goto() 只允許挑 3 次。這個限制是每個範圍的限制。 因此進出當前範圍都會重置跳轉計數器為 0 。另外,每個動作標籤都有獨立的跳轉計數器。

丟擲來的例外的記號是 too-many-tries,可以用下面的 Ops 程式碼捕獲:

goal all {
    run {
        say("before"),

    again:

        say("middle"),
        goto(again),
        {
            too-many-tries =>
                say("caught the too-many-tries exception");
        },
        say("after");
    }
}

這個程式(假設檔名是 goto2.ops)可以成功完成:

$ opslang --run goto2.ops
before
middle
middle
middle
middle
caught the too-many-tries exception
after

回到目錄

跳轉計數限制

我們已經看到預設的跳轉限制是 3 次,對相同標籤的第四次 goto 呼叫會導致too-many-tries 例外。 我們可以透過給 goto 函式呼叫傳遞命名引數 tries: N 來修改這個限制,如下所示:

goto(again, tries: 10);

這時候我們有 10 次的限制而不是 3 次。

除了限制跳轉的次數,我們還可以在每次 goto 之前增加可變的延遲。如下所示:

goto(again, init-delay: 0.001, delay-factor: 2, max-delay: 1,
     max-total-delay: 10);

這兒顯示了我們在每次 gotoagain 標籤之前,都增加了越來越大(睡眠)的延遲: 從 0.001 秒開始,直到 1 秒,每次重試都增大兩倍。一旦前面所有嘗試的總延遲積累到超過了 10 秒, 將會丟擲 too-many-tries 例外。

請注意第一次 goto() 呼叫是不會有任何延遲或者睡眠的。init-delay 設定只有在第二次嘗試(或者是第一次重試)的時候起作用。

請參考 goto 函式的文件 獲取更多細節。

回到目錄

標籤宣告

前面的例子都使用 goto() 函式向後跳轉(跳轉的目標動作在 goto 呼叫之前)。 使用者也可以向前跳轉。這種場景下,標籤必須用 label 語句宣告,否則標籤就會在宣告之前使用。如下:

goal all {
    run {
        label done;

        say("before"),
        goto(done),
        say("middle"),

    done:

        say("after");
    }
}

在這裡,done 標籤在目標 allrun 動作階段一開始就宣告瞭。

執行此程式得出(假設檔名是 goto-fwd.ops):

$ opslang --run goto-fwd.ops
before
after

請注意動作 say("middle") 實際上被前面的 goto() 函式忽略掉了,而標籤東走 say("after") 還是會執行的。

回到目錄

從檢查階段跳轉到動作階段

我們可以使用 goto() 跳轉到定義在其它範圍內的標籤上,條件是這個跳轉沒有離開當前的全域性動作階段, 或者沒離開當前使用者動作的宣告體。

所以從一個檢查階段裡面跳轉到一個周圍的動作階段是很自然的事情,如下所示: following example demonstrated:

goal all {
    run {
        label do-work;

        check {
            ! file-exists("/tmp/a") =>
                goto(do-work);

            true => ok;
        }

    do-work:

        say("hi from all.run");
    }
}

如果檔案 /tmp/a 不存在,那麼這個程式生成輸出:

hi from all.run

否則不會有輸出生成。

有時候我們希望不管檢查階段的 ok() 呼叫結果如何都有條件執行所有動作階段的目標。 這個功能可以用 opslang 的。-B 命令列選項實現,如下:

opslang -B --run foo.ops

或者是以編譯器:

opslang -B -o foo.lua foo.ops

在這個勾子下, -B 選項會讓 ok 函式的行為會表現得跟 nok 函式完全一樣。 這個功能在開發除錯 Ops 程式的時候很有用。

回到目錄

規則範圍的標籤

規則有自己的範圍,所以如果你想在單個規則的後面部門使用前向標籤宣告和 goto 函式的話, 你需要使用標籤宣告動作語法,如下:

goal all {
    run {
        label done;

        check {
            true =>
                label done,
                say("check pre"),
                goto(done),
                say("NOT THIS"),
            done:
                say("check post");
        }

        say("run pre"),
        goto(done),
        say("NOT THIS"),
    done:
        say("run post");
    }
}

執行這個函式得到如下輸出:

check pre
check post
run pre
run post

請注意我們可以在不同範圍裡有同名標籤。

回到目錄

運算子

支援下列運算子,按照優先順序順序列出:

優先順序               運算子
0                   後包圍 [], {}, <>
1                   **
2                   單目 +/-/~, as
3                   * / % x
4                   + - ~
5                   << >>
6                   &
7                   | ^
8                   單目 !, > < <= >= == !=
                    contains contains-word prefix suffix
                    !contains !contains-word !prefix !suffix
                    eq ne lt le gt ge
9                   ..
10                  ?:

使用者可以用圓括弧 () 在一個表示式裡頭明確修改相關優先順序或者關聯性。

回到目錄

算術運算子

此語言支援下列雙目運算子:

**      指數
*       乘積
/       除
%       模除
+       加
-       減

比如:

2 ** (3 * 2)        # 得出 64
(7 - 2) * 5         # 得出 25

還支援單目字首運算子 - 如:

-(3.15 * 2)         # 得出 -6.3

回到目錄

字串運算子

本語言支援下列雙目字串運算子:

x       重複字串多次並且把他們連線起來
~       字串連線

比如:

"abc" x 3           # 得出 "abcabcabc"
"hello" ~ "world"   # 得出 "helloworld"

回到目錄

位運算子

支援下列位運算子:

<<          左移
>>          右移
&           位 AND
|           位 OR
^           位 XOR

單目字首運算子 ~ 是用於對位的 NOT 操作。不要把他跟字串連線的雙目運算子。~ 混了。

回到目錄

關係運算子

所有關係運算子的使用都會為當前表示式產生布林結果。 使用關係運算子的表示式是關係表示式

下列雙目運算子在數值上比較兩個運算元:

>           大於
<           小於
<=          小於或等於
>=          大於或等於
==          等於
!=          不等於

下列雙目運算子在字母順序上比較兩個運算元:

gt          大於
lt          小於
le          小於或等於
ge          大於或等於
eq          等於
ne          不等於

在字串值裡頭可用 3 個特殊的雙目運算子進行模式匹配:

contains            如果運算子右手邊的運算子“包含於”左手邊的運算子,為真

contains-word       如果右手邊的運算子以字的形式“包含於”左手邊的運算子,為真

prefix              如果右手邊的運算子是左手邊運算子的“字首”,為真

suffix              如果右手邊的運算子是左手邊運算子的“字尾”,為真

單目字首運算子 ! 對(布林)運算元取反

如果在字串比較運算子右手邊的運算元長得像一個模式或者一個萬用字元, 那麼會自動在模式中假設匹配錨,比如:

uri eq rx{ /foo } =>
    say("hit");

等效於:

uri contains rx{ \A /foo \z } =>
    say("hit");

不過,如果正則模式 \A 只匹配字串開頭,而 \z 只匹配結尾。那麼 contains 運算子則不會假設有匹配錨。

類似, contains-word 會假設在使用者正則的兩邊有包圍的 \b 正則錨。

回到目錄

範圍運算子

雙目運算子 .. 可以用於生成範圍表示式,如:

1 .. 5          # 等效於 1, 2, 3, 4, 5
'a'..'d'        # 等效於 'a', 'b', 'c', 'd'

範圍表示式的值是在該範圍內所有值平面化的列表。

這個運算子尚未實現!

回到目錄

三元運算子

三元關係運算子 ?: 用於根據使用者條件,在兩個使用者表示式之間選擇。

比如:

$a < 3 ? $a + 1 : $a

這個表示式,在 $a < 3 為真得時候,計算得出 $a + 1 的值,否則是 $a

回到目錄

下標運算子

後包圍運算子 [] 用於下標一個陣列。比如:

my @names = ('Tom', 'Bob', 'John');

true =>
    say(@names[0]),  # 輸出 Tom
    say(@names[1]),  # 輸出 Bob
    say(@names[2]);  # 輸出 John

負數的下標索引用於從陣列末尾訪問元素,比如,-1 是最後一個元素, -2 是倒數第二個元素,等等。

類似,後包圍運算子 {} 用於索引一個雜湊表,如:

my %scores = (Tom: 78, Bob: 100, John: 91);

true =>
    say(%scores{'Bob'});    # output 100

後包圍運算子 <> 用於透過字面串來訪問一個雜湊表, 比如,%scores<John> 等效於 %scores{'John'}

這個運算子尚未實現!

回到目錄

規則

規則在 Ops 語言裡提供基本的控制流語言結構。 他們替換了傳統語言裡頭的 if/else 語句,讓複雜的分支控制流程式碼更容易讀寫。

Ops 的下列語言環境中,可以使用規則:

  1. 動作塊,和
  2. 流塊

回到目錄

基本規則佈局

Ops 語言的規則由兩部分組成,一個條件和一個後果。 條件和後果之間由一個 => 連線,整條規則由一個分號字元結束。基本的規則長得像下面這樣:

<condition> => <consequent>;

規則的條件部分可以接受一個活多個關係表示式,類似 resp-status == 200。所有關係表示式都是透過逗號字元連線的 (,),它令所有關係表示式在一起,也就是說,要想整個條件為真,那麼所有關係表示式都必須為真。 條件不能有副作用,這個屬性是 opslang 編譯器強制的,因此,同一個條件裡關係表示式中的計算順序並不改變整個條件的結果。

後果部分通常包含一個或多個動作。每個動作都可以有一些副作用, 比如修改一些請求,執行一些 302 重定向,或者修改當前請求在後臺中的路由路徑。 我們也可以把一個完整的規則塊宣告成一個動作(參閱動作塊一節)。

下面是一個簡單的 opslang 規則:

file-exists("/tmp/a.txt") =>
    $ rm /tmp/a.txt;

在條件部分, file-exists("/tmp/a.txt") 是一個關係表示式。 file-exists() 函式檢查其引數宣告的檔案路徑是否存在。

這條規則的意思是如果檔案 /tmp/a.txt 存在,則刪除它。 後果部分是一個美元符動作,執行 shell 命令 rm /tmp/a.txt

值得一提的是 file-exists() 函式接受一個位置引數而不是命名引數

Ops 語言是一種自由格式語言,所以你可以隨意使用空白。 上面例子後果部分中的縮排空白不是必須的,只是為了美觀。我們完全可以把整個規則寫成一行,比如:

file-exists("/tmp/a.txt") => $ rm /tmp/a.txt;

回到目錄

多重關係表示式

使用者可以在一個條件裡頭宣告多個關係表示式,比如:

file-exists("/tmp/a.txt"), hostname eq 'tiger' =>
    say("hit!");

這裡我們在條件中多了一個關係表示式,也就是: hostname eq 'tiger',它用標準函式 hostname() 測試當前主機名(“hostname”)是否等於 'tiger'。 在這個例子裡我們用了一個不同的動作 say("hit!"),執行的時候它會給當前執行 opslang 程式的標準輸出流傳送一個 “hit!”。

請注意兩個關係表示式之間的逗號,它意味著,如果想要整個條件為真,那麼逗號兩邊的關係表示式都必須為真。

使用者可以在同一個條件裡宣告更多的關係表示式,如:

file-exists("/tmp/a.txt"), hostname eq 'tiger', prompt-user eq 'agentzh' =>
    say("hit!");

我們加了第三個關係表示式,它測試最後一次 shell 提示符字串後面的顯示值是否當前使用者名稱 (這個通常是用標準函式 setup-sh()自動配置的)。請注意我們也可以用標準函式 prompt-host 替換 hostname 函式。 prompt-host 函式呼叫通常比 hostname 更快, 因為它無須執行一個新的 shell 命令(hostname()需要執行 shell 命令 hostname)。

回到目錄

多重條件

Ops 規則實際上可以接受多個並行的條件,它們之間透過分號運算子連線。 這些條件邏輯上是為當前規則在一起的。

比如:

file-exists("/foo"), prompt-host eq 'glass';
file-exists("/bar"), prompt-host eq 'nuc'
=>
    say("hit!");

如果兩個條件之一匹配上了,那麼規則就算匹配上。當然,如果兩個條件都匹配, 規則也匹配。

回到目錄

多重動作

在同一個規則後果裡,也可以宣告多個動作。 看看下面的例子:

file-exits("/tmp/foo") =>
    chdir("/tmp/"),
    $ rm -f foo,
    say("done!");

這個例子在後果裡頭有三個動作。第一個動作呼叫內建函式 chdir() 把當前工作目錄修改到 /tmp/。第二個動作是一個 美元符動作, 它以同步的方式執行 shell 命令 rm -f foo (不過從 IO 的角度看,仍然是非阻塞的)。最後, 呼叫 say() 函式輸出 done! 給當前 opslang 程式的 stdout 流。

回到目錄

無條件規則

有些規則可以選擇無條件執行它們的動作。不過一個 opslang 規則, 總是要求一個條件部分。為了實現無條件規則出發,使用者可以使用總是為真的斷言 true() 作為條件中單一的關係表示式,如:

true() =>
    say("hello world");

在這個規則裡頭,動作 say() 將總是執行。

因為 opslang 函式呼叫在不帶引數的時候可以忽略元括弧,我們可以寫 true 而不用 true(),如:

true =>
    say("hello world");

回到目錄

多重規則

在同一個塊裡頭宣告的多個規則,是按順序執行的。 先寫的規則先執行。

看看下面的例子:

file-exists("/tmp/foo") =>
    say("file exists!");

file-not-empty("/tmp/foo") =>
    say("file not empty!");

如果有一個 /tmp/foo,檔案,並且裡頭有一些資料,執行這個 opslang 程式碼會生成下面的輸出:

file exists!
file not empty!

不過,為了能同時匹配,多重規則的條件可能會被 opslang 編譯器最佳化,甚至可能在任何規則世紀執行之前就計算。 這些在 opslang 編譯器覺得這麼做安全的時候可能發生。

回到目錄

動作塊

動作塊由一對花括弧({})組成,同時也生成一個新的變數範圍。 動作塊可以在所有可以用動作的地方使用。

在下面的例子裡,我們有兩個不同的 $a 變數,因為他們屬於不同的塊(範圍):

my Int $a = 3;

{
    my Str $a = "hello";
},
say("a = $a");   # output `3` instead of `hello`

規則和變數一樣,從詞法上屬於所包含的塊。 動作塊可以用做把相近的規則組合在一起成為一個整體。在這樣的設定裡, 一些提前執行的規則可以使用特殊動作 done忽略所有同一個塊裡頭的後繼東旭哦。 下面例子顯示了這個用法:

{
    file-exists("/tmp/a.txt") =>
        print("hello"),
        done;

    true =>
        print("howdy");
},
say(", outside!");

如果檔案 /tmp/a.txt 在當前系統中存在,那麼這個 Ops 程式碼片段的輸出會是: hello, outside!。 請注意第一個規則裡的 done 動作忽略了第二個規則的執行。 如果檔案 /tmp/a.txt 不存在,那麼它會生成輸出 howdy, outside!,因為第一條規則不匹配。

規則塊可以任意巢狀,如:

file-exists("a") =>
    say("outer-most"),
    {
        true =>
            say("2nd level");
            {
                uri("b") =>
                    say("3rd level");
            };
    };
};

回到目錄

非同步塊

Ops 語言支援並行執行多個終端會話。這個是透過使用 async 塊實現的。 在 async 塊裡頭的動作會透過一個新的輕量化執行緒在一個新的 終端螢幕裡非同步執行。

看下面的例子:

goal all {
    run {
        my Int $c = 0;

        $ FOO=32,
        async {
            $ echo in async,
            print(cmd-out);
        },
        $ echo in main,
        print(cmd-out);
    }
}

典型的執行生成:

$ opslang --run async.ops
in main
in async

有時候我們可能會得到互換的輸出行:

$ opslang --run async.ops
in async
in main

這個的執行結果是預期中的,因為 async {} 塊執行的非同步特質。

非同步塊自動建立一個與當前獨立的新的終端視窗。 實際上,它在當前鉤子下呼叫標準函式 new-screen。 因此,可以建立的非同步塊總數也受 opslang 工具的 --max-screens N 引數的限制。 這個引數顯然可以調整。

禁止切換到當前輕量執行緒不擁有的終端會話。如果嘗試這麼幹,會收到一個類似下面的報錯:

test.ops:11: ERROR: screen 'foo' not owned by the current thread

非同步塊可以巢狀或使用,也可以在使用者動作歷史用。非同步塊也可以建立自己的終端視窗。

非同步塊也可以有一個名字,如:

async foo {
    ...
}

這樣的話,對應非同步塊的終端視窗也可以接受同樣的名字。 否則他們獲得類似 123等等這樣的序列號名稱。 具體幾號去絕育他們在 ops 原始碼檔案中出現的順序。

終端螢幕的日誌檔案 script 遵循和用 new-screen 函式明確建立終端視窗一樣的命名規則。 對於非同步終端,他們有特殊的終端名如 1, 2, 3 這樣的。

Ops 程式在所有 async {} 塊和主執行緒退出之前不會停止。不過,有幾個例外:

  1. 如果執行執行緒呼叫了標準函式 exit() 的時候,
  2. 如果執行執行緒呼叫了標準函式 die() 的時候,或者
  3. 在一個執行執行緒有一個未捕獲的例外,比如 timeout 的時候。

回到目錄

執行緒之間共享變數

所有 Ops 程式都有一個輕量執行緒,就是啟動的時候建立的主執行緒。 使用者可以透過 async {} 塊生成更多清涼執行緒。所有這些執行緒共享同樣的透過 my 宣告的頂層變數。 參考下面的例子:

my Int $c = 0;

goal all {
    run {
        setup-sh,
        $ FOO=32,
        async {
            setup-sh,
            $c++,
            say("c=", $c);
        },
        $c++,
        say("c=", $c);
    }
}

執行這個例子總是給出下面輸出:

c=1
c=2

動作階段或者使用者動作裡頭定義的本地變數,如果在 async {} 塊裡可見, 那麼在 async {} 塊派生的時候會立即複製到一個複製里頭。 在新執行緒裡,進入 async 塊到時候,這些本地變數仍然具有他們的初始值,但是給它們賦值的時候將不會再影響其它執行緒, 其它執行緒的變數賦值也不會影響這個新執行緒。比如:

goal all {
    run {
        my Int $c = 0;

        setup-sh,
        $ FOO=32,
        async {
            setup-sh,
            $c++,
            say("async: c=", $c);
        },
        sleep(0.1),
        $c++,
        say("main: c=", $c);
    }
}

執行這個例子總會生成下列輸出:

async: c=1
main: c=1

所以實際上這倆執行緒每個都有自己本地的 $c

回到目錄

賦值動作

賦值運算子 = 用於宣告一個動作,這個動作可以給一個變數賦值,或則說一個表示式可以是左值。比如:

my $a;

$a = 3;

和所有其它動作一樣,賦值表示式自己沒有值。 所以不允許把賦值表示式巢狀在其它表示式裡頭。 比如,下面的例子會生成一個編譯時錯誤:

? my $a;
?
? say($a = 3);

這是因為賦值語句 $a = 3 不返回值,所以它只能當一個獨立的動作使用。

下面的賦值:

$a = $a + 3

可以用運算子 += 簡化:

$a += 3

類似的還有 *=, /=, %=, x=, ~= 分別用於雙目運算子 *, /, %, x, 和 ~

另外,字尾運算子 ++ 可用於簡化 += 1。 比如:

$a++

等效於 $a += 1$a = $a + 1。類似的還有字尾運算子 --,等效於 -= 1

類似標準的 = 運算子,所有這些賦值運算子的變種自己都不接受任何數值,只能用於獨立的動作中。

回到目錄

Lua 塊

在 opslang 程式裡內聯 Lua 程式碼是希望讓複雜的事情有可能實現。

Lua 程式碼片段包含在Lua 塊裡,可以用在全域性範圍,用作一個動作, 或者一個 ops 表示式。

回到目錄

全域性 Lua 塊

全域性 lua 塊用在當前 .ops 檔案(是否 Ops 模組都行)的頂級範圍。 這樣的 Lua 程式碼片段將在生成的 Lua 原始碼檔案的頂級範圍內一個獨立的 Lua 變數範圍內擴充套件。 任何在 Lua 程式碼塊裡直接定義的 Lua 本地變數都不會被這個 lua 塊以外所見。 如果你想定義整個檔案的 lua 程式碼都可見的 Lua 變數,那麼變數名用 _M.NAME 而不要用 NAME

下面是一個簡單的例子:

lua {
    print("init!")
}

goal all {
    run {
        say("done");
    }
}

執行這個 global-lua-block.lua 例子給出:

$ opslang --run global-lua-block.lua
init!
done

回到目錄

Lua 塊動作

Lua 塊也可以直接當做動作使用。這種場合下,不會預期有返回值從 Lua 塊中返回。下面是一個例子:

goal all {
    run {
        print("hello, "),
        lua {
            print("world!")
        };
    }
}

執行這個 lua-action.ops 例子給出:

$ opslang --run lua-action.ops
hello, world!

回到目錄

Lua 塊表示式

Lua 塊可以用作 Ops 表示式,但在這種場合下,必須宣告返回型別,並且內聯的 Lua 原始碼必須返回一個對應資料型別的 Lua 值,像下面這樣:

goal all {
    run {
        say("value: ", lua ret Int { return 3 + 2 })
    }
}

執行這個 lua-expr.ops 例子給出:

$ opslang --run lua-expr.ops
value: 5

回到目錄

Lua 塊中的變數代換

我們可以在上述所有三種 Lua 塊中替換 opslang 的變數。

所有下列形式的 Ops 變數都可以在內聯 Lua 程式碼中支援:

  1. $.NAME
  2. $.{NAME}
  3. $NAME
  4. ${NAME}
  5. @.NAME
  6. @.{NAME}
  7. @{NAME}

如果 Ops 變數在 Lua 的單引號或者雙引號包圍字串裡使用的時候,在最後的 Lua 值裡,他們會保持原來的字串值, 即使他們的字串值包含特殊字元,比如 ', ", 和 \ 也如此。 另外一方面,在 Lua 的圓括弧長字串裡,Ops 變數不會被代換。

代換出來的 Ops 變數可以用在所有可用 Lua 表示式的 Lua 程式碼裡。

下面是一個在 Lua 的單引號包圍字串字面裡頭代換 Ops 陣列變數的例子:

goal all {
    run {
        my Str @foo = ("hello", 'world');

        say("lua: ", lua ret Str {
            return '[@foo]'
        })
    }
}

執行之給出輸出:

lua: [hello world]

下面是一個代換 Ops 標量變數的例子:

goal all {
    run {
        my Str $foo = "hello";
        my Int $bar = 32;

        say("lua: ", lua ret Str {
            return $foo .. $.bar + 1
        })
    }
}

執行這個例子給出下面輸出:

lua: hello33

回到目錄

For 語句

for 語句是一種用於便利陣列或者雜湊容器的特殊型別的動作。其通用語法如下:

for CONTAINER -> VAR1 [, VAR2] {
    SYMBOL-DECLARATION;
    SYMBOL-DECLARATION;
    ...

    ACTION,
    ACTION,
    ...;
}

下面是遍歷一個 Int 陣列的例子:

goal all {
    run {
        my Int @arr = (56, -3, 0);

        say("begin"),
        for @arr -> $elem {
            say("elem: $elem");
        },
        say("end");
    }
}

執行這個生成下列輸出

begin
elem: 56
elem: -3
elem: 0
end

請注意我們不需要透過 my 關鍵字給迴圈變數 $elem 做宣告,因為 for 語句的 -> 運算子已經在 for 語句的範圍內隱含定義了這個本地變數。

還要注意我們不用宣告迴圈變數 $elem 的型別,因為其型別總是在 -> 運算子生效之前判定為容器表示式生成的陣列或雜湊的元素型別。

這裡離子裡我們也可以定義兩個迴圈變數,用於併發迴圈遍歷:

goal all {
    run {
        my Int @arr = (56, -3, 0);

        say("begin"),
        for @arr -> $i, $elem {
            say("elem $i: $elem");
        },
        say("end");
    }
}

得出:

begin
elem 0: 56
elem 1: -3
elem 2: 0
end

同樣,我們不用宣告 -> 後面變數 $i 的型別, 因為其型別總是 Int

我們可以用類似方法遍歷一個雜湊表,這時候兩個迴圈變數必須分別宣告為雜湊表的鍵和值,比如:

goal all {
    run {
        my Int %a{Str} = (dog: 3, cat: -51);

        say("begin"),
        for %a -> $k, $v {
            say("key: $k, value: ", $v);
        },
        say("end");
    }
}

生成下列輸出:

begin
key: dog, value: 3
key: cat, value: -51
end

回到目錄

終端模擬器操作

Ops 語言的最強的地方是內建支援任意終端模擬器的自動化。 它對 *NIX 風格的終端的透明操作控制序列支援力度極高。在啟動的時候, 所有 Ops 程式都會為後面的操作建立一個偽終端模擬器例項(就是 pty)。 透過這個辦法,Ops 程式可以跟人類操作者一樣與作業系統的終端介面進行互動。 預設的時候,這個終端是與互動的 bash 會話繫結的,這個 shell 可以透過命令列工具 opslang--shell SHELL 選項修改。

本文裡我們會互換使用 終端終端模擬器 兩個屬於。 兩個術語都表示前述的偽終端模擬器的意思。

從某種角度來說,Ops 語言提供的終端互動能力跟經典的 Tcl/Expect 工具鏈提供的執行時工具非常類似, 只是在很多方面具有更強功能和更靈活。

完整的終端互動細節都記錄在一個命令列工具 script (通常來自 util-linux 專案)生成的日誌檔案裡。 在命令列執行 Ops 程式之後,檢查 ./ops.script.log 檔案通常可以提供很多有用資訊。 這個日誌檔案路徑可以透過 opslang 命令列工具的 --script-log-file PATH 選項修改。

回到目錄

向終端寫出

Ops 程式可以透過標準函式 send-text 給終端傳送文字資料,比如 echo hello。 它還可以透過標準函式 send-key 給終端傳送特殊控制字元序列(比如回車鍵或者 Ctrl-C 組合鍵)。

回到目錄

輸入回顯

需要注意的是,很多時候我們向終端寫出資料的時候,終端會透過回顯輸入的東西給(人類使用者)出相應(雖然並不總是如此)。 更糟糕的是,有時候如果敲擊太快,因環境不同,回顯的輸出甚至會全部或者部分重複。 Ops 程式總是需要準備處理從終端返回的這種“回顯”輸出。不過,幸運的是, Ops 程式的確提供了一些內建的特性和函式來幫助處理這種無邊的細節,所以 Ops 程式設計師通常不需要關心這些問題, 除非想親自在非常底層處理每個小細節。

回到目錄

從終端讀取

從終端讀取天生要比向終端寫出更難。這是因為終端流輸出的資料內容和大小通常都是不可預測的。 Ops 語言提供兩種讀取終端資料的辦法:流的方式和全緩衝的方式。 Ops 語言根據具體的使用場景,可以隨意選擇任何一種方式讀取。

回到目錄

流讀取

Ops 語言提供流塊用於從終端模擬器中讀取流資料。

stream 塊裡,標準函式 out 以一種抽象的方式代表終端的輸出流。 這個out 函式呼叫可以用於雙目關係表示式,進行正則和字串模式匹配(相關的 運算子contains, prefix, eq, 和 suffix)。 千萬不要把 out 的值當作普通字串值使用;否則會看到下面的編譯錯誤:

ERROR: out(): this function can only be used as the lvalue of relational expression operators at out.ops line 5

out 流自動過濾所有在真實控制檯中用於顯示或者打扮的很花哨的控制檯控制序列。 如果你希望處理這些未加修改的終端控制序列,你應該使用標準函式 raw-out

還有一個 echoed-out 變種,它是用於匹配輸出留中的鍵入回顯的。

回到目錄

流塊

流塊的格式如下:

stream {
    rule-1;
    rule-2;
    ...
}

基本上,一個流塊以一個關鍵字 stream 開頭,然後跟著一個用一對花括弧包圍的塊,在這個 塊裡可以宣告一個或多個規則

看看下面的例子:

goal all {
    run {
        stream {
            out suffix /.*?For details type `warranty'\.\s*\z/ =>
                print($0),
                break;
        }
    }
}

如果用這個 Ops 程式跟無所不在的 UNIX bc 一起像下面這樣執行 (假設 Ops 程式在檔案 bc-banner.ops 裡頭):

$ opslang --shell bc --run bc-banner.ops
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.

這個 Ops 程式先讀取 bc 的初始旗標輸出(由 bc 程式自己在終端列印出來), 然後輸出到自己的 stdout (標準輸出)裝置。 輸出中的實際文字可能因你係統的 bc 程式的不同版本(甚至不同來源)而異。

請注意前面例子 stream {} 塊裡面那唯一一個規則。 我們使用文字For details type `warranty`. 作為旗標語的結尾。 一旦匹配了這個正則,我們就透過列印特殊捕獲變數 $0 的值作為輸出。 這個值包含所有這個正則匹配上的東西。

請注意上面規則裡頭的 break 動作。這個函式呼叫立即終止當前 stream {} 塊的讀取動作,否則 stream 塊會持續迴圈等待更多規則匹配 (直到 timeout 例外丟擲)。

流塊可以有多條規則,這些規則和那些規則將以流處理的方式獨立與終端輸出資料流進行匹配。 讓我們看看另外一個處理 bc 程式的例子:

goal all {
    run {
        stream {
            out contains /\w+/ =>
                say("rule 1: $0");

            out contains /\w+/ =>
                say("rule 2: $0");

            out suffix /For details type `warranty'\.\s*/ =>
                say("quitting..."),
                break;
        };
    }
}

執行這個程式產生下面輸出:

rule 1: bc
rule 2: bc
quitting...

因此所有 3 個規則都輪流執行。規則執行的順序很重要:頭兩個規則都匹配 out 資料流同樣位置相同的子字串, 因此它們執行的順序取決於它們在 Ops 程式原始碼中出現的順序。 結果是第一條規則總是在第二條規則之前執行。而另外一方面,第三條規則匹配 out 資料流中的最後的語句, 所以它總是在頭兩條規則之後執行,讓我們試著把第三條規則挪到頭兩條規則之前, 看看發生甚麼:

goal all {
    run {
        stream {
            out suffix /For details type `warranty'\.\s*/ =>
                say("quitting..."),
                break;

            out contains /[a-zA-Z]+/ =>
                say("rule 1: $0");

            out contains /[a-zA-Z]+/ =>
                say("rule 2: $0");
        };
    }
}

執行這個程式得到一樣的輸出:

rule 1: bc
rule 2: bc
quitting...

第一條規則的匹配文字的位置在後面兩條規則匹配文字位置之後, 所以即使第一條規則在 Ops 原始碼中先出現,它也在後面兩條規則之後執行。 這是流處理的本質。我們不會等著全部輸出完畢才進行匹配。我們總是一看到點輸出就開始匹配。

還要注意前面的例子裡,後兩個規則並不包括 break 動作。這麼做很重要,因為 break 會立即中斷當前 stream{} 的執行流,而我們實際希望的是這個例子能儘可能多匹配和執行規則。 如果我們給 stream 塊裡所有三個規則都加上 break 動作:

goal all {
    run {
        stream {
            out suffix /For details type `warranty'\.\s*/ =>
                say("quitting..."),
                break;

            out contains /[a-zA-Z]+/ =>
                say("rule 1: $0"),
                break;

            out contains /[a-zA-Z]+/ =>
                say("rule 2: $0"),
                break;
        };
    }
}

那麼很自然,在第一個有機會執行的規則上(假設程式碼存到了檔案 all-break.ops 裡):

$ opslang --shell bc --run all-break.ops
rule 1: bc

另外一個對前面例子的很重要的觀察是,所有 stream 塊裡頭的規則, 都至多執行一次。顯然,條件 out contains /[a-zA-Z]+/ 會在 bc 程式的旗標文字中匹配多次, 即使如此,我們也只獲得對輸出的第一次匹配。這是 Ops 語言設計如此。 如果想讓規則匹配多次,我們應該在對應規則的後果部分呼叫 redo 動作,如:

goal all {
    run {
        stream {
            out suffix /For details type `warranty'\.\s*/ =>
                say("quitting..."),
                break;

            out contains /[a-zA-Z]+/ =>
                say("word: $0"),
                redo;
        };
    }
}

執行這個 redo.ops 程式獲得下列輸出:

$ opslang --shell bc --run redo.ops
word: bc
word: Copyright
word: Free
word: Software
ERROR: redo.ops:10: too-many-tries
  in stream block rule at redo.ops line 10
  in rule/action at redo.ops line 4

這次,我們透過 redo 動作匹配了更多“單詞”:bc, Copyright, Free, 和 Software。 有趣的是,Ops 程式在輸出 4 個單詞之後,帶著一個未捕獲例外 too-many-tries 退出了。 丟擲 too-many-tries 例外是因為該規則裡頭的 redo 動作執行了超過 3 次。 標準函式 redo 的預設重試限制是 3,跟標準函式 goto 一樣。要在前述例子裡匹配和輸出更多單詞, 我們可以為 redo() 呼叫宣告 tries 命名引數。如:

goal all {
    run {
        stream {
            out suffix /For details type `warranty'\.\s*/ =>
                say("quitting..."),
                break;

            out contains /[a-zA-Z]+/ =>
                say("word: $0"),
                redo(tries: 20);
        };
    }
}

這次我們可以輸出 bc 旗標語中的所有單詞,而不會被 too-many-tries 例外丟擲:

word: bc
word: Copyright
word: Free
word: Software
word: Foundation
word: Inc
word: This
word: is
word: free
word: software
word: with
word: ABSOLUTELY
word: NO
word: WARRANTY
word: For
word: details
word: type
word: warranty
quitting...

現在讓我們用 bc 乾點兒真實有用的工作,比如,執行簡單的算術計算 3 + 5 * 2

goal calc {
    run {
        my Str $expr;

        stream {
            out suffix /For details type `warranty'\.\s*/ =>
                break;
        },

        # enter the expression:
        $expr = "3 + 5 * 2",
        send-text($expr),
        stream {
            echoed-out suffix $expr =>
                break;
        },

        # submit the entered expression by pressing Enter:
        send-key("\n"),
        stream {
            echoed-out prefix /.*?\n/ =>
                break;
        },

        # collect the result in the new output:
        stream {
            out suffix /^(\d+)\n/ =>
                say("res = $1"),
                break;
        };
    }
}

這個例子有點長,因為它直接處理了裸終端的互動。首先我們用流塊等待 bc 輸出所有旗標文字。 然後我們輸入透過標準函式 send-text 表示式 3 + 5 * 2。 之後我們用另外一個流塊等待終端回顯我們剛剛“敲入”的表示式文字。 一旦我們確認已經輸入了表示式,我們透過呼叫 send-key 函式模擬一個鍵盤迴車輸入(跟人類使用者一樣)。 然後,我們使用一個新的流塊確保回車鍵按下並且回顯給我們。最後,我們使用最後一個流塊來接收 bc 輸出的計算結果。

請注意我們使用了標準函式 echoed-out 來匹配我們的鍵入回顯。 ehoed-outout 主要的區別是前者會自動過濾掉終端自動折行引入的特殊字元序列 (每個終端都有固定寬度,如果鍵入的行寬超過寬度限制,就會折行)。

你可能會奇怪,為啥我們要等待輸入的回顯。這是因為我們所有與 bc 的溝通都要透過終端模擬器, 如果我們鍵入太快,我們可能就丟失一些鍵入,就好像打字很快的人類在響應緩慢的終端上偶爾會丟失一些鍵入一樣。

執行這個 calc.ops 程式生成

$ opslang --shell bc --run calc.ops
res = 13

讓我們檢查裸終端會話歷史裡頭的 ./ops.script.log 檔案:

$ cat ops.script.log
Script started on 2019-06-12 17:10:52-07:00
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
3 + 5 * 2
13

這就是我們預期的輸出。

回到目錄

全緩衝讀取

全緩衝的終端輸出讀取是透過定義一個合理的提示符,然後讀取標準函式 cmd-out 返回的字串值實現的。 cmd-out 返回的字串值定義為前一個 stream {} 塊的 out 流位置到透過當前 stream {} 塊匹配上的萬用字元字串之間的東西。

參考 GDB 提示符 一節獲取例子。

回到目錄

提示符處理

在前面的 bc 的例子裡,我們已經看到如何直接在 Ops 裡處理與終端的互動。 對那些更復雜的互動程式,比如 bashgdb,我們需要處理“提示字串”(或者就是“提示符”), 比如 bash-4.4$(gdb) 這樣的。 如果我們在每個 Ops 程式裡都手工處理這些提示符,那將會是一個無比枯燥和艱鉅的任務。 幸運的是,Ops 透過各種標準函式,比如 push-promptpop-prompt, 還有標準 例外 found-prompt 等,提供內建的提示符處理的支援。

回到目錄

GDB 提示符

看看下面的例子,它敲入一些 gdb 命令並輸出其結果:

goal all {
    run {
        my Str $cmd;

        push-prompt("(gdb) "),

        # wait for the prompt to show up:
        stream {
            found-prompt => break;
        },

        # send our gdb command:
        $cmd = "p 3 + 2 * 5",
        send-text($cmd),

        # wait for our typing to echo back:
        stream {
            echoed-out suffix $cmd =>
                break;
        },

        # press Enter to submit the command:
        send-key("\n"),
        stream {
            echoed-out prefix /.*?\n/ =>
                break;
        },

        # collect and output the result:
        stream {
            found-prompt =>
                print("res: ", cmd-out),
                break;
        };
    }
}

在這個例子裡,我們先用標準函式 push-prompt 給 Ops 執行時註冊一個信的提示符字串模式,"(gdb)"。 然後再後面的 stream {} 塊裡頭,我們可以用一個帶著條件 found-prompt 的規則等待終端輸出流中出現的第一個這樣的提示符字串。 一旦我們獲得了 gdb 提示符,我們就傳送 gdb 命令,p 3 + 2 * 5,等待輸入回顯,然後敲回車鍵, 然後獲得回車的輸入回顯,最後等待下一個 gdb 提示符。拿到新的 gdb 提示符之後, 我們用標準函式 cmd-out 為前面執行的命令抽取輸出(定義為當前匹配上的提示符字串之前,以及前一個流塊輸出流(本例是回車鍵敲入之後的回顯)輸出的位元組之後的資料)。 使用 cmd-out 函式實際上是全緩衝讀模式。 很明顯它是基於偵測到提示字串和預設流讀取模式的。

我們需要注意我們提示字串模式裡頭 (gdb) 後面的空白字元。 那個空白是必須的,因為提示符字串匹配總是一個 suffix (字尾)匹配操作,而 GDB 總是在 (gdb) 字串後頭立即放一個空白。 它是真實 GDB 提示符的一部分。

我們執行這個 gdb.ops 程式,用 gdb 作為我們的 shell:

$ opslang --shell gdb --run gdb.ops
res: $1 = 13

正是我們要的。

實際上我們可以用標準函式 send-cmd 簡化我們上面的例子。 這個函式組合了傳送命令字串,等待輸出回顯,敲入回車鍵以及等待新行回顯的所有操作。 前例簡化以後的版本如下:

goal all {
    run {
        push-prompt("(gdb) "),

        # wait for the prompt to show up:
        stream {
            found-prompt => break;
        },

        # send our gdb command:
        send-cmd("p 3 + 2 * 5"),

        # collect and output the result:
        stream {
            found-prompt =>
                print("res: ", cmd-out),
                break;
        };
    }
}

現在的程式碼短了很多,結果完全一樣:

$ opslang --shell gdb --run gdb2.ops
res: $1 = 13

回到目錄

Bash 提示符

Bash 的提示符比 GDB 的更復雜,因為它就不是一個固定的字串。 它可以由使用者配置(比如透過給 bash 變數設定一個 PS1 模版),因而可能因為提示符模版裡頭的不同 shell 變數而包含動態的資訊。

下面是一個嘗試匹配真實 bash 提示符字串的例子:

goal all {
    run {
        stream {
            out suffix rx:x/^ [^\r\n]+ [\$%\#>)\]\}] \s*/
            =>
                push-prompt($0),
                break;
        },
        send-cmd("echo hello"),
        stream {
            found-prompt =>
                break;
        },
        say("out: [", cmd-out, "]");
    }
}

在這個例子裡,我們先用一個流塊和一個正則模式匹配嘗試搜尋一個長得像 shell 提示符的字串。 然後我們把匹配上的字串透過標準函式 push-prompt 推到棧裡頭。 push-prompt 函式的一個特性是它自動把提示符字串裡頭的數字規範化為 0, 所以就算 shell 提示符模版包含類似 $? 這樣的變數,我們的提示符檢測依舊會成功。

讓我們用 --shell bash 選項執行這個 sh-prompt.ops 程式:

$ opslang --shell bash --run sh-prompt.ops
out: [hello
]

Ops 語言提供好幾個額外的標準函式讓我們更容易處理 Bash 會話。比如, 前面例子可以用 shell 包圍字串 簡化:

goal all {
    run {
        stream {
            out suffix rx:x/^ [^\r\n]+ [\$%\#>)\]\}] \s*/ =>
                push-prompt($0),
                break;
        },
        sh/echo hello/,
        stream {
            found-prompt =>
                break;
        },
        say("out: [", cmd-out, "]");
    }
}

然後我們還可以用美元符動作進一步簡化:

goal all {
    run {
        stream {
            out suffix rx:x/^ [^\r\n]+ [\$%\#>)\]\}] \s*/ =>
                push-prompt($0),
                break;
        },
        $ echo hello,
        say("out: [", cmd-out, "]");
    }
}

甚至最初的 shell 提示符檢測都可以簡化為標準函式 setup-sh 的呼叫:

goal all {
    run {
        setup-sh,
        $ echo hello,
        say("out: [", cmd-out, "]");
    }
}

當然, setup-sh 函式可以比上面的窮人版 shell 提示符檢測乾的事兒更多。

實際上,setup-sh 幾乎總是任何有 shell 會話互動的 Ops 程式的第一個屌用, 因而,如果使用者沒有宣告 --shell SHELL 命令列引數的話, opslang 命令會自動呼叫之。 如果 opslang 命令列工具已經自動呼叫了 setup-sh 函式,那就要確保你不會再你的目標動作階段之初重複呼叫此函式。 重複呼叫會導致讀取超時的錯誤,因為在 shell 會話開始的時候, 不會有任何重複的 shell 提示符字串出現。

因此,如果我們不像前例那樣帶著 --shell bash 選項執行命令列工具 opslang, 那麼我們必須從我們 Ops 程式裡刪除 setup-sh 呼叫:

goal all {
    run {
        $ echo hello,
        say("out: [", cmd-out, "]");
    }
}

執行這個 sh-prompt2.ops 輸出如下:

$ opslang --run sh-prompt2.ops
out: [hello
]

這個例子終於成為了最精簡的樣子。

回到目錄

巢狀 Bash 會話

Ops 語言的一個強大的地方是處理巢狀 shell 會話時的強有力。下面是一個例子:

goal all {
    run {
        sh/bash/,
        setup-sh,
        $ echo $(( 3 + 1 )),
        say("out: [", cmd-out, "]"),
        exit-sh;  # to quit the nested shell session
    }
}

執行這個 nested-sh.ops 給出下面輸出:

$ opslang --run nested-sh.ops
out: [4
]

這裡我們必須為巢狀的 shell 會話明確呼叫 setup-sh 並且呼叫標準函式 exit-sh 進行合理的退出。 exit-sh 函式實際上在鉤子上做下面的兩個動作:

pop-prompt,
$ exit 0;

標準函式 pop-prompt 刪除當前巢狀 shell 會話的提示符模式並且恢復父 shell 會話的提示符模式設定。 然後它執行 shell 命令 exit 0,然後 Ops 執行程式就可以成功檢測到父 bash 會話的提示符字串了。

需要注意的是我們有意在第一個 bash shell 命令裡避免使用了美元符動作。 請記住上面的 shell 包圍的字串動作 sh/bash/ 只是全稱 send-cmd(sh/bash/) 的一個縮寫註解。 我們在這裡絕不能bash 命令使用美元符動作,因為美元符動作會自動在一個隱含流塊裡頭等待提示符字串。 在這裡我們絕對需要一個 setup-sh() 呼叫為新的 shell 會話自動檢查一個新的提示符字串(不是舊會話的提示符)。

參考上面例子的方式,我們可以在 Ops 程式會話裡巢狀任意多 shell 會話。 push-promptpop-prompt 函式呼叫在一個內部的提示符模式棧上運作,可以支援任意深度的增長。

巢狀 bash 會話也可以透過執行其它 shell 命令,比如 su,而不是直接呼叫 bash 來建立。

回到目錄

巢狀 SSH 會話

對付巢狀 SSH 會話跟 巢狀 bash 會話一樣簡單。下面是一個例子:

goal all {
    run {
        sh/ssh -o StrictHostKeyChecking=no 127.0.0.1/,
        setup-sh,
        $ echo $(( 3 + 1 )),
        say("out: [", cmd-out, "]"),
        exit-sh;  # to quit the nested ssh session
    }
}

執行這個 nested-ssh.ops 程式輸出:

$ opslang --run nested-ssh.ops
out: [4
]

當然,現實世界裡連線到本地主機沒啥用處。 我們可以選擇連線遠端伺服器。

跟巢狀 bash 會話一樣,巢狀 SSH 會話也可以巢狀任意深度,比如,

goal all {
    run {
        sh/ssh -o StrictHostKeyChecking=no 127.0.0.1/,
        setup-sh,
        $ echo $(( 3 + 1 )),
        say("1: out: [", cmd-out, "]"),

        sh/ssh -o StrictHostKeyChecking=no 127.0.0.1/,
        setup-sh,

        $ echo $(( 7 + 8 )),
        say("2: out: [", cmd-out, "]"),

        exit-sh,  # to quit the second ssh session
        exit-sh;  # to quit the first ssh session
    }
}

執行這個程式給出輸出

1: out: [4
]
2: out: [15
]

這個功能對那些需要先登入中間“跳板機”再登入最終伺服器的環境特別有用。

回到目錄

多終端螢幕

Ops 語言支援在同一個程式裡建立多個終端螢幕,類似流行的命令列工具 GNU screentmux

所有輸出和提示符處理都在獨立的視窗裡進行。 不過 Ops 變數是共享的。

我們可以透過標準函式 new-screen() 建立新的終端視窗,然後透過 switch-screen 函式在多個視窗之間切換。

實際上,Ops 總是建立名為 “main” 的預設視窗。

視窗名必須是有效的識別符號。使用非法的名字串會導致執行時或者編譯時錯誤。

參考下面例子:

goal all {
    run {
        # in the default 'main' screen
        $ Foo=,

        new-screen('foo'),

        # in our new 'foo' screen
        $ FOO=32,

        switch-screen('main'),

        say("main: FOO: [", sh-var('FOO'), "]"),

        switch-screen('foo'),

        say("foo: FOO: [", sh-var('FOO'), "]");
    }
}

在這裡我們建立了第二個叫 foo 的螢幕,然後我們在預設的 main 螢幕裡頭, 把 shell 變數 FOO 設定為空串,在我們的 foo 螢幕裡頭設定為 32. 這個例子在這兩個不同螢幕裡頭輸出這個 FOO shell 變數的值。 我們使用標準函式 sh-var 檢索特定 shell 變數的值。

執行這個 screens.ops 程式輸出:

$ opslang --run screens.ops
main: FOO: []
foo: FOO: [32]

自然,不同終端視窗有不一樣的 shell 環境。

請注意 new-screen() 函式呼叫也會自動切換到新建立的螢幕。

switch-screen() 函式呼叫的一個重要的行為習慣是它隻影響到當前使用者動作或者當前使用者目標動作階段結束。 switch-screen() 的作用將持續到超越 check {} 階段,直到周圍動作階段塊結束。

單個 Ops 程式能建立的並行視窗數有一個限制。預設的時候是 20,它可以透過命令列引數 --max-screens 來設定, 如下:

opslang --run --max-screens 100 foo.ops

所生成的指令碼日誌檔案也是每個螢幕獨立的。預設時 main 螢幕的日誌檔案依舊是 ./ops.script.logfoo 的日誌檔案是 ./ops.script.log.foo。使用者宣告的指令碼日誌檔案路徑會遵循相同的命名方式 (非 main 螢幕會在 main 檔名後面追加螢幕名)。

回到目錄

使用者動作

使用者可以透過把一些其它動作組合在一起,定義自己的實用動作。 定義客戶化動作的常用愈發如下:

action <name> (<arg>...) [ret TYPE] [is export] {
    [<label-declaration> | <variable-declaration>]...

    <action1>,
    <action2>,
    ...
    <actionN>;
}

例子:

action say-hi(Str $who) {
    say("hi, $who!"),
    say("bye");
}

呼叫這個 say-hi 動作: say-hi("Tom"),會生成輸出:

hi, Tom!
bye

可以宣告多個動作。

使用者定義動作是一個給動作引入自己的詞表、並用於規則或者其它環境的好辦法。

禁止遞迴動作。

引數必須帶著型別資訊宣告,這點跟那些使用 my 關鍵字宣告的本地變數一樣。 可選的引數可以用預設值宣告,如:

action say-hi(Str $who = "Bob") {
    say("hi, $who");
}

然後 say-hi 動作就可以不帶引數呼叫,如 say-hi(),在此場合下,引數 $who 將獲得預設值 "Bob"

我們可以給這樣的可選引數宣告預設值 nil,如:

action foo(Int $a = nil) {
    {
        defined($a) =>
            say("a has a value!"),
            done;

        true =>
            say("a has no value :(");
    }
}

我們也可以使用標準函式 defined 來檢查一個變數是否為 nil 。

回到目錄

帶返回值的使用者動作

使用者動作可以返回使用者宣告型別的值。方法是在引數列表後立即宣告 ret TYPE 性質,如:

action world() ret Str {
    return "world";
}

請注意我們是如何使用 return 語句把字串值 "world"返回給這個 world 使用者動作的呼叫者的。

如果省略了 ret TYPE 性質,那麼就假設不返回值或者返回 nil 值。

回到目錄

目標

opslang 的目標有點類似 Makefile 的目標,不過他們從不像 Makefile 的目標那樣與任何檔案關聯。 在某種意義上,他們更類似 GNU Makefile 的“偽目標”。 Ops 的目標是一種命名任務,是 Ops 程式每次執行要實現的任務。 他們是 Ops 程式的入口(類似 C 程式的 main 函式)。

每個 Ops 程式至少需要定義一個目標,當然,也可以定義多個目標。 定義了多個目標的 Ops 程式,他們也會有多個入口。 預設入口稱作預設目標。通常,預設目標時在 Ops 程式裡定義的第一個目標,這個預設目標可以透過 default goal 語句覆蓋。

Ops 裡定義的最簡單的目標類似下面:

goal hello {
    run {
        say("hello, world!");
    }
}

這段程式碼定義了一個新的叫 hello 的目標,在執行的時候給當前 Ops 程式的標準輸出流列印出 hello, world! 行。它也是 Ops 語言世界的經典“hello world”程式。如果這個程式在檔案 hello.ops 裡頭,我們可以這樣執行:

$ opslang --run hello.ops
hello, world!

正是我們想要的。

這個例子裡我們有些東西要注意:

  1. 我們在 hello 目標裡定義裡一個 run {} 塊,它呼叫了一個 動作階段.一個目標可以有幾個不同型別的動作階段,比如 run, build, install, prep, 和 packagerun 動作階段是目標最常用的使用者動作階段。 如果其它型別的動作階段不合理,那麼就使用 run
  2. 本例的 run 動作階段包含一個動作,就是一個函式呼叫 say("hello, world!")
  3. 程式檔案 hello.ops 只包含這一個目標,hello,所以,如果執行這個 Ops 程式的時候沒有透過命令列引數宣告其它目標(透過 opslang命令), 那麼這個目標也就死活預設目標。

我們可以在 opslang 命令列上明確宣告制定的目標:

opslang --run hello.ops hello

額外的命令列引數 hello 宣告要執行的目標。

我們也可以透過更多命令列引數宣告多個目標,如:

opslang --run example.ops foo bar baz

在這個例子裡,目標 foo, bar, 和 baz 會按順序執行。

回到目錄

預設目標

每個 Ops 程式都必須有且只能有一個預設目標, 如果在命令列或者 Lua API 呼叫裡沒有宣告目標,那麼就會執行這個預設目標。 預設目標可以用 default goal 語句明確宣告,類似下面這樣:

default goal foo;

這個語句必須在 Ops 程式的頂層範圍裡,通常在整個檔案的開頭附近。 目標 foo 可以在 default goal 語句後面定義。 沒有要求 foo 必須定義在這個 default goal 語句之前。

如果 Ops 程式裡頭沒有 default goal 語句,那麼第一個定義的目標就會成為預設目標。

回到目錄

動作階段

一個目標可以一個或多個動作階段,一個動作階段是一個命名的動作組合,當執行目標時會執行這個動作組合。 下面是預定義的動作階段,按通常執行的順序列出:

  • prep: 做些準備工作。
  • build: 進行軟體製作(通常是程式碼編譯和生成)。
  • install: 安裝軟體。
  • run: 執行軟體或其它無法分類的動作。
  • package: 給製作和安裝好的軟體打包(如 RPM 或者 Deb 包)。

一個目標哦可以定義一個或多個動作階段,或者也可以不定義。

如果一個目標沒有定義動作階段,那麼它就是一個抽象動作階段, 那麼就一定有一些目標依賴

要明確呼叫一個目標 foo 的動作階段 xxx,我們只需要用全稱引用那個階段即可:foo.xxx。 比如,要引用 foo 的動作階段 run,我們寫 foo.run()。圓括弧可以想普通 Ops 函式呼叫那樣忽略掉。

如果一個動作定義了多個動作階段,那麼這些動作階段會形成一個鏈。 如果一個靠後的董總階段準備執行,那麼任何已定義的在這個動作階段之前的動作階段都會按序執行。 比如,如果一個目標 foo 定義了有 prep, build, 和 run 階段, 然後呼叫者執行 foo.run 階段。在 foo.run 階段執行的時候,目標會自動線執行 foo.prep, 然後 foo.build。而 foo.install 階段不會執行,因為在這個 foo 的例子裡頭, 沒有定義這個階段。

也可以不宣告動作階段名呼叫目標,就把目標當作一個函式就行。比如, 要想屌用目標 foo,我們寫 foo()

結果是屌用此目標的 預設動作階段

回到目錄

動作階段的本地符號

動作階段可以定義自己的本地符號,類似 變數標籤。這樣的符號如果定義在當前動作階段,那麼也可以被[檢查階段](#檢查階段)塊可見。

下面是一個例子:

goal hello {
    run {
        my Str $name = "Yichun";

        say("Hi, $name!");
    }
}

執行這個目標生成輸出:

Hi, Yichun!

回到目錄

預設動作階段

如果一個目標被呼叫,但是沒有宣告動作階段, 那麼將會呼叫預設動作階段。

預設動作階段定義為 1) 除了 packageprep 階段之外的最後執行階段, 或者 2) package 階段,如果它是唯一定義的階段。

比如,如果一個目標 foo 定義裡動作階段 build, install, run, 和 package,然後預設動作階段將會是 run 階段。另外一個例子,如果目標 bar 只定義了 package 階段,那麼將會使用預設的動作階段。

不定義任何動作階段的抽象目標將智慧執行其依賴的目標。

回到目錄

檢查階段

每個動作階段都可以接受一個可選的檢查階段, 它執行一系列規則來判斷當前動作階段的主要動作是否應該執行。 檢查階段通常用於驗證當前目標是否已經制作或滿足。

檢查階段環境裡可以用兩個標準函式來發出成功或者失敗的訊號。 他們是 oknok 函式。呼叫 ok() 函式會導致當前檢查階段立即成功, 因此當前動作階段會被忽略。另外一方面, nok() 函式導致當前檢查階段立即失敗, 然後開始執行周圍動作階段的主動作。

如果檢查階段沒有呼叫任何這裡啊函式,那麼預設時檢查階段失敗(也就是說, 需要執行動作階段的主動作)。

考慮下面這個簡單的例子:

goal hello {
    run {
        check {
            file-exists("/tmp/a.txt") =>
                ok();
        }
        say("hello, world!");
    }
}

在終端執行這個 Ops 程式:

$ rm -f /tmp/a.txt

$ opslang --run hello.ops
hello, world!

$ touch /tmp/a.txt

$ opslang --run hello.ops

$

第一次執行因為不存在 /tmp/a.txt 檔案,導致 check {} 階段失敗, 因此 run {} 動作階段的主動作 say("hello, world!") 就被執行並且一行一行輸出。 第二次執行的時候,因為 檔案 /tmp/a.txt 已經存在, check 階段成功,因此主動作這次不再執行。

回到目錄

目標依賴

一個目標可以指明一些其它目標為自己的依賴項。 在當前目標執行之前,這些依賴目標的預設動作階段會被檢查和執行。

比如,

goal foo: bar, baz {
    run { say("hi from foo") }
}

goal bar {
    run { say("hi from bar") }
}

goal baz {
    run { say("hi from baz") }
}

目標 foo 定義了兩個依賴, barbaz。 這兩個目標會在 foo 執行之前執行。我們在終端上測試這個例子(假設這個例子在檔案 deps.ops 裡):

$ opslang --run deps.ops
hi from bar
hi from baz
hi from foo

回到目錄

目標引數

使用者動作 一樣,目標可以定義自己的引數。 引數的語法非常類似使用者動作的語法。比如:

default goal all;

goal foo(Int $n, Str $s) {
    run {
        say("n = $n, s = $s");
    }
}

goal all {
    run {
        foo(3, "hello"),
        foo(-17, "world");
    }
}

執行這個 Ops 程式生成下面輸出:

n = 3, s = hello
n = -17, s = world

我們也可以直接從命令列介面直接呼叫引數化的目標。看看下面的例子:

goal foo(Int $n, Str $s) {
    run {
        say("n = $n, s = $s");
    }
}

假設這個檔名為 param-goal.ops,我們可以這樣執行:

$ opslang --run param-goal.ops 'foo(32, "hello")'
n = 32, s = hello

在目標宣告器裡頭使用的單引號和雙引號字串遵循 Ops 語言自己同樣的語法規則。 只是在這個環境裡變數代換是總被禁止的。看看下面的例子:

$ opslang --run param-goal.ops 'foo(0, "hello\nworld!")'
n = 32, s = hello
world!

$ opslang --run param-goal.ops "foo(0, 'hello\nworld')"
n = 32, s = hello\nworld

這裡也支援布林字面 truefalse

這個環境裡,值型別必須準確匹配引數型別,因為不會做自動的型別轉換。

在編譯器模式下,我們可以做下面的事情:

$ opslang -o param-goal.lua param-goal.ops

$ resty -e 'require "param-goal".main(arg)' /dev/null 'foo(56, "world")'
n = 56, s = world

回到目錄

目標記憶

對於任何單個 Ops 程式的執行,每個目標都會最多執行一次。 Ops 的執行時程式將會跟蹤目標的成功執行(或檢查),這樣不會有目標被多次執行。

對於帶引數的目標,每個目標的每個實際引數值都會記錄一次,因此 foo(32)foo(56) 都會被執行。

這也是目標和使用者動作之間的最大的不同。

看下面的例子:

goal foo {
    run {
        say("hi from foo");
    }
}

goal all {
    run {
        foo,
        foo,
        foo;
    }
}

執行這個例子將智慧產生一行輸出:

hi from foo

這是因為後面兩個 foo 目標因目標結果的記憶而略過了。

回到目錄

從目標中返回

我們可以使用 return 語句從目標的主動作鏈中返回。 只是目標目前還不支援返回任何值。可以使用頂級變數儲存目標執行的結果。

回到目錄

模組

Ops 語言支援模組化程式設計,這就意味著它可以很容易構造大型的程式。

每個 Ops 模組都必須在自己的 .ops 檔案裡。比如, 模組 foo 應該在單獨的檔案 foo.ops 裡,而 模組 foo.bar 應該在檔案 foo/bar.ops 裡。

每個 Ops 模組檔案應該以一個宣告模組名都 module 語句開頭,比如:

module foo;

或者

module foo.bar;

模組名必須跟檔案路徑名指定的一樣。

要想從其它 Ops 模組或者 Ops 主程式檔案中裝載 Ops 模組, 只要使用use 語句即可,如下:

use foo;
use foo.bar;

第一個語句在模組搜尋路徑查詢 foo.ops 檔案然後裝載 foo 模組, 而第二個是找 foo/bar.ops 檔案。

每個模組都只能裝載一次。對同一個模組的多個 use 語句是 no-op

模組 foo 裡頭的動作,例外和目標總是可以透過其它 .ops 檔案引用,方法是用全稱, 比如 foo.bar,這裡 foo 是模組名,而 bar 是定義在 foo 模組裡的符號名。

回到目錄

模組搜尋路徑

Ops 程式在一個目錄列表裡搜尋 Ops 模組檔案。 這個目錄列表稱作模組搜尋路徑。 預設搜尋路徑包含兩個目錄:

  1. 當前工作目錄, ..
  2. <bin-dir>/../opslib 目錄,這裡的 <bin-dir> 是儲存 opslang 命令列可執行檔案自身的忙碌。這個預設的搜尋路徑是用於裝載標準的 Ops 模組 std,這個模組定義許多標準函式

使用者可以透過一次或多次宣告 -I PATH 選項,給 opslang 命令列工具新增自己的路徑,比如:

opslang -I /foo/bar -I /blah/ ...

透過 -I 選項新增的新路徑將被追加到現有搜尋路徑上。 請注意路徑的順序是有意義的。 Ops 執行時會直接使用第一個匹配上的路徑,忽略其它的。

回到目錄

Std 模組

Ops 編譯器自帶一個標準 Ops 模組,叫做 std。此 std 模組定義了許多標準函式,預設的時候會自動裝載到 Ops 程式中,除非透過命令列工具 opslang 的選項 --no-std-lib 關閉之。

回到目錄

輸出模組符號

模組中定義的類似 目標, 使用者動作,和 使用者例外這些符號,可以輸出給模組的裝載器。 方法是給對應的使用者動作、使用者例外或者目標等的宣告新增 is export 性質。如:

module foo;

action do-this () is export {
    say("do this!");
}

然後在住程式檔案 main.ops 裡,我們可以裝載這個模組並且馬上開始使用輸出的使用者動作 do-this()

use foo;

goal all {
    run {
        do-this();
    }
}

執行這個程式生成如下輸出:

do this!

不管一個符號是否輸出,我們都可以用其全稱引用一個裝載上來的模組的符號。 比如前面的例子:

use foo;

goal all {
    run {
        foo.do-this();
    }
}

輸出目標也類似:

module foo;

goal foo (Str $name): bar, baz is export {
    run {
        say("running foo: $name!");
    }
}

goal bar {
    run {}
}

goal baz {
    run {}
}

請注意一個輸出目標的依賴關係並不自動輸出。 如果你需要輸出這些依賴關係,那麼必須給那些目標都加上 is export 性質。

要輸出一個使用者例外,我們可以寫:

exception foo is export;

請注意不要輸出太多符號,因為會汙染裝載者的名字空間。

回到目錄

命令列介面

TODO

回到目錄

Lua API

TODO

回到目錄

標準函式

Ops 語言提供豐富的標準函式集。這些函式有些是透過編譯器直接內建的, 其它一些是透過 標準模組實現的。

使用者可以用自己的同名使用者動作覆蓋這些內建函式。 不過通常不建議這麼做,因為有可能破壞 Ops 語言的核心。

下面是字母順序的所有標準函式的文件。

回到目錄

append-to-path

assert

assert-host

assert-user

bad-cmd-out

basename

break

ceil

centos-ver

chdir

check-git-uncommitted-changes

chomp

clear-prompt-host

clear-prompt-user

closed

cmd-out

ctrl-c

ctrl-d

ctrl-v

ctrl-z

cwd

deb-codename

default-read-timeout

default-send-timeout

defined

die

dir-exists

do-ctrl-c

done

echoed-out

elems

error

exit

exit-code

exit-sh

failed

false

file-exists

file-modification-time

file-not-empty

file-size

floor

found-prompt

goto

home-dir

hostname

is-regular-file

join

new-screen

nil

nok

nop

now

ok

os-is-amazon

os-is-deb

os-is-debian

os-is-fedora

os-is-redhat

os-is-ubuntu

out

pop

pop-prompt

prepend-to-path

print

prompt

prompt-host

prompt-user

push

push-prompt

raw-out

read-timeout

redhat-major-ver

redo

return

say

send-cmd

send-key

send-text

send-timeout

setup-sh

sh-var

shift

sleep

strlen

substr

switch-screen

throw

timeout

to-int

to-num

too-many-tries

too-much-out

true

unshift

warn

whoami

with-std-prompt

回到目錄

作者

章亦春 <yichun@openresty.com>, OpenResty Inc.

回到目錄

版權 & 許可證

Copyright (C) 2019 by OpenResty Inc. All rights reserved.

本文件是專屬文件,包含商業機密資訊。任何時候均禁止未經版權所有者書面許可重新分發本文件。

回到目錄