Skip to main content

基础语法:目标10分钟上手

Yak 是一门纯 Golang 的嵌入式语言,是一门纯 Golang 实现的基于反射特性的语言,语法在一定程度上保留了 Golang 的风格,甚至可以实现 Golang 语言的对象无缝开放给 Yak 使用的功能。

Golang 的一些令人惊叹的特性我们在 Yak 中也可以找到实现,比如大家熟悉或者有所耳闻的 go func() {} ()defer 关键字,chan <T> 等。

在保留这些优秀功能的同时,我们极大的简化了 Golang 的语法,移除了静态类型的特性,移除了严格的语法检查,这是一门动态强类型语言,同时,我们也尽力兼容了一些动态语言常见的使用场景。

最令人激动的其实是我们在语言内置了各种安全从业人员需要的安全扫描工具和前无古人的模糊测试模块。

我们希望 Yak 对与安全从业人员是相当友好的,10 分钟即可上手!如果你熟悉 Golang 和 Python,我相信 Yak 一定也是你将会非常喜爱的胶水语言。

yak 每行结尾不需要加分号,当然加了也没关系

运算符支持#

基础运算符#

// 加减乘除:  1 + 4 * 5'+' '-' '*' '/' 
// 取余数'%'
// 赋值运算符:为保持 Golang 用户使用习惯,我们保留了 := 赋值模式,a := 1; 或者 a = 1 是等效的'=' ':=' 
// 位运算'^' '&' '&^' '|' '<<' '>>'
// 运算赋值,值得注意的是,++ 和 -- 只能用在变量后,其相当于 += 1 / -= 1'+=' '-=' '*=' '/=' '%=' '++' '--'
// 位运算赋值'^=' '&=' '&^=' '|=' '<<=' '>>='
// 单位运算符,取反'!'
// 逻辑比较运算符'>=' '<=' '>' '<' '==' '!=' '&&' '||'

特殊运算符,操作 Golang 通道:<-#

<- 操作符既可以把元素写入现成的 chan 中,也可以从 chan 中读取相关元素

  1. varName = <- chanVar 意思是把 chanVar 这个 chan 读出一个元素,赋值给 varName。
  2. chanVar <- someValue 意思是把 someValue 写入 chanVar 这个 chan 中

如果读者有写过 Golang,我们下面一段代码可以更容易帮你理解这个特殊操作符

// 声明一个 chan,类型为 var, chan 的缓冲区为 2,可以缓冲两个元素ch := make(chan var, 2)ch <- 123ch <- 456
firstIn = <- chsecondIn = <- chprintln("first:    ", firstIn)println("seconds:  ", secondIn)

我们很容易猜到,上面代码的执行结果为

first:     123seconds:   456

类型与数据结构使用#

基础数据类型#

1. int2. float     // 等效于 Golang 的 float643. string4. byte5. bool6. var       // 等效于 Golang 的 interface{}          

这些类型其实都是最基础的数据类型:

// 以下是一组基础声明a = 1 // 创建一个 int 类型变量,并初始化为 1b = "hello" // string 类型c = true // bool 类型d = 1.0 // float 类型e = 'h' // byte 类型f = var // f 为一个 兼容类型

复合类型#

  1. slice:是用于以列表的形式存储元素的数据类型 (等效于 Python 的 list),同 Golang 中的 slice
  2. map:用于以键值对的形式存储数据的结构(等效于 Python 中的 dict),同 Golang 中的 map
  3. chan:Yak 与 Golang 特有的复合数据类型,通过 make(chan <T>) 创建,<T> 为这个 chan 数据通道传输的数据类型,可以类比为 Python 中的 Queue

函数类型与类#

  1. 闭包与函数均是特殊类型的实例,同 Golang 与 Python,我们可以把函数赋值给一个变量,在你想使用它的时候使用它
  2. 类与类成员函数也是本语言可支持的特色之一,但是对于脚本语言来说,OOP 的特性支持会极大增加语言复杂程度,尽管语言支持,但是作者并不希望这个特性被广泛使用。

如何使用 Slice / List#

声明一个 Slice#

b = [1, 2.3, 5]         // 创建一个 []floatc = ["a", "b", "c"]     // 创建一个 []stringd = ["a", 1, 2.3]       // 创建一个 []var (等价于 Go 语言的 []interface{})e = make([]int, 3, 3)   // 创建一个 []int,并将长度设置为 3,容量设置为 3f = make([][]int, 3, 3) // 创建一个 [][]int,并将长度设置为 3,容量设置为 3g = []byte{1, 2, 3}     // 创建一个 byte slice,并初始化为 [1, 2, 3]h = []byte(nil)         // 创建一个空 byte slice

Slice / List 的各种操作#

  1. 含义与 Golang 相同的 append 用法
a = append(a, 1, ,2, 3)
  1. 通过 len 获取 Slice 的元素个数,通过 cap 获得 slice 的容量,同 Golang 用法
a = append([1,2,3], 5,6,7)length = len(a)println("len(a): ", length)
capValue = cap(a)println("cap(a): ", capValue)
// OUTPUT://   len(a):  6//   cap(a):  6
  1. copy(sliceA, sliceB) 把 sliceB 的元素复制到 sliceA 中,如果复制的元素个数为 min(len(sliceA), len(sliceB))
a = [1,2,3]b = [4,5,6,7,8]copiedCount = copy(a, b)println("copy(a, b) returns ", copiedCount)println("slice a now: ", a)
// OUTPUT://   copy(a, b) returns  3//   slice a now:  [4 5 6]
  1. 按索引取数组元素,支持拆包,具体案例如下
a = [1,2,3,4,5,6,7,8]b = a[4:6]c = a[:5]d = a[3:]index1 = a[1]index2, index3 = a[2], a[3]
println("b: ", b)println("c: ", c)println("d: ", d)println("a[1]: ", index1)println("a[2]: ", index2)println("a[3]: ", index3)

上述脚本输出的结果为

b:  [5 6]c:  [1 2 3 4 5]d:  [4 5 6 7 8]a[1]:  2a[2]:  3a[3]:  4
  1. 拆包都是基本操作

下面是一个 slice 拆包的基本案例

a, b, c = [1, 2.3, "stringValue"]println("a: ", a)println("b: ", b)println("c: ", c)
/*OUTPUT:
a:  1b:  2.3c:  stringValue*/ 

当然,拆包+赋值的另一个案例读者可以尝试

a = make([]var, 4)a[1], a[2], a[3] = 1, "asdfasdf", false
println("slice a: ", a)
/*OUTPUT:
slice a:  [<nil> 1 asdfasdf false]*/

如何使用 Map / Dict#

赋值与创建一个 Map / Dict#

a = {"a": 1, "b": 2, "c": 3}             // 得到 map[string]int 类型的对象b = {"a":1,"b":2.3,"c": 3}               // 得到 map[string]float64 类型的对象c = {1: "a", 2: "b", 3: "c"}             // 得到 map[int]string 类型的对象d = {"a": "hello", "b": 2.0, "c": true}  // 得到 map[string]interface{} 类型的对象e = make(map[string]int)                 // 创建一个空的 map[string]int 类型的对象f = make(map[string]map[string]int)      // 创建一个 map[string]map[string]int 类型的对象g = map[string]int16{"a": 1, "b": 2}     // 创建一个 map[string]int16 类型的对象x = {}                                   // 创建一个 map[string]interface{}x = make(map[var]var)                    // 创建一个 map[interface{}]interface{}

上述方法均可创建一个 map,我们发现:

  1. 直接使用 {key: value ... } 方式创建的 map 会自动选择最兼容的类型

  2. 使用 make(map[T1]T2) 可以创建指定类型的 map

    小贴士 通过 {"key": "value", ... } 方式创建的 map,key 必须是 string

map / dict 的基本操作#

a = {"a": 234, "b": "sasdfasdf", "ccc": "13"}      // 我们创建一个 map[var]varn = len(a)                                         // 取 a 的元素个数println("len(a): ", n)// OUTPUT: len(a):  3
x = a["b"]                                         // 取 a map 中 key 为 "b" 的元素println(`a["b"]: `, x)// OUTPUT: a["b"]:  sasdfasdf
a.noSuchKey                                        // 如果取一个不存在的 key,直接通过 .keyName 调用则会报错,退出程序// OUTPUT: //     [ERRO] 2021-05-25 20:38:04 +0800 [default:yak.go:100] line 58: member `noSuchKey` not found
// 当然,我们也可以把拆包解包用在 map 中a["e"], a["f"], a["g"] = 4, 5, 6                   // 同 Go 语言a.e, a.f, a.g = 4, 5, 6                            // 含义同 a["e"], a["f"], a["g"] = 4, 5, 6
// 如果你想要删掉 map 中的某个元素a = {"a": 123, "b": 1345, "c": 999}println("a.b: ", a.b)delete(a, "b")println("NOW map a: ")dump(a)/*a.b:  1345NOW map a:(map[string]int) (len=2) { (string) (len=1) "a": (int) 123, (string) (len=1) "c": (int) 999}*/ 

如何判断元素是否存在?

注意

注意:在判断元素是存在的时候,Yak 和 Golang 有区别

x = {"a": 1, "b": 2}a = x["a"] // 结果:a = 1if a != undefined {             // 判断a存在的逻辑    }c = x["c"]                      // 结果:c = undefined,注意不是0,也不是nild = x["d"]                      // 结果:d = undefined,注意不是0,也不是nil

chan 操作#

chan 的创建只能通过 make(chan T, [capBuffer: int])#

创建 chan 与 Golang 保持相同的方法

ch1 = make(chan bool, 2)        // 得到 buffer = 2 的 chan boolch2 = make(chan int)            // 得到 buffer = 0 的 chan intch3 = make(chan map[string]int) // 得到 buffer = 0 的 chan map[string]int

chan 的基础操作#

/*创建一个 chan var*/ch1 = make(chan var, 4)ch1 <- 1ch1 <- 2
// 一通操作n = len(ch1)               // 取得chan当前的元素个数m = cap(ch1)               // 取得chan的容量v = <-ch1                  // 从chan取出一个值close(ch1)                 // 关闭chan,被关闭的chan是不能写,但是还可以读(直到已经写入的值全部被取完为止)v1 = <- ch1v2 = <- ch1
// 查看操作的结果和特性println("len(ch1): ", n)println("cap(ch1): ", m)println("<-ch1 执行第一次: ", v)println("<-ch1 执行第二次: ", v1)println("<-ch1 执行第三次: ", v2)

上述脚本执行结果为:

len(ch1):  2cap(ch1):  4<-ch1 执行第一次:  1<-ch1 执行第二次:  2<-ch1 执行第三次:  undefined
chan 使用小贴士 需要注意的是,在 chan 被关闭后,<-ch 取得 undefined 值。所以在 yak 中应该这样:
v = <-ch1if v != undefined { // 判断chan没有被关闭的逻辑    ...}

使用函数/闭包:def func fn 均等效#

函数和闭包的定义#

函数和闭包定义关键字为 def / func / fn, 这三个关键字完全等效!

// 定义了一个函数为 aaa,同时这个函数被赋值给了 bbb,所以这个函数拥有了两个名字bbb = func aaa() {    println("具名函数 aaa 函数赋值给了 bbb,被调用了")}
// 定义了个匿名函数,赋值给了 ccc,所以 ccc 可以被当作函数调用了ccc = fn() {    println("匿名函数赋值给了 ccc,被调用了")}
// 分别执行 aaa  bbb  ccc 来看结果aaa()bbb()ccc()
// 声明一个闭包def{    println("这是一个闭包,声明的时候,将立即会被调用")}
// 让 Goroutine 执行一个闭包函数go def{    sleep(0.5)    println("在 Gorouting 中执行了闭包函数喔")}
// aaa() 放在 Gourinte 中执行go aaa()
// sleep 1 秒等待 Goroutine 执行完成sleep(1)

执行完如上脚本,我们看到结果为

具名函数 aaa 函数赋值给了 bbb,被调用了具名函数 aaa 函数赋值给了 bbb,被调用了匿名函数赋值给了 ccc,被调用了这是一个闭包,声明的时候,将立即会被调用具名函数 aaa 函数赋值给了 bbb,被调用了在 Gorouting 中执行了闭包函数喔

Yak 函数的定义:定义参数与可变参数#

本质上 yak 函数的定义本质上都是 func(params ...interface{}) interface{} 因此可以对常见任何形态的函数定义做兼容。

// 我们定义一个带参数的函数func x(a) {    println("exec in x func, recv: ", a)}println(x("aaa"))
/*OUTPUT:
exec in x func, recv:  aaa*/
func argsTest1(vars...) {    println("argsTest1 recv: ", vars)}
func argsTest(var1, vars...) {    println("var1: ", var1)    println("vars: ", vars)}
argsTest1(123, 1, 2, 3, 4, 5)println("---------------------")argsTest(123, 1, 2, 3, 4, 5)
/*argsTest1 recv:  [123 1 2 3 4 5]---------------------var1:  123vars:  [1 2 3 4 5]*/

函数的返回值:可以支持多个返回值,拆包#

当我们定义的 yak 函数有多个返回值是,我们默认返回的是一个列表 []interface{},所以在函数返回的时候,可以支持自动拆包,这样做间接支持了 Golang 的多返回值语法。

// 我们看如下案例func testFunc(arg1, args...) {    return 1, 2, arg1}
rets = testFunc("test")println("rets = testFunc(`test`) -> rets: ", rets)println("---------------")ret1, ret2, ret3 = testFunc("aaa", "bbb", "ccc")println(`ret1, ret2, ret3 = testFunc("aaa", "bbb", "ccc")`)println("ret1: ", ret1)println("ret2: ", ret2)println("ret3: ", ret3)
/*rets = testFunc(`test`) -> rets:  [1 2 test]---------------ret1, ret2, ret3 = testFunc("aaa", "bbb", "ccc")ret1:  1ret2:  2ret3:  aaa*/

rets = testFunc("test") 的情况下, 我们发现 rets 的值为 [1 2 test] 直接印证了我们的说法。

当然在不完全拆包的情况下,会报错,我们看如下情况

func testFunc(arg1, args...) {    return 1, 2, arg1}ret1, ret2 = testFunc("aaa", "bbb", "ccc")
/*OUTPUT:
[ERRO] 2021-05-26 23:32:19 +0800 [default:yak.go:100] line 4: multi assignment error: require 3 variables, but we got 2*/

Go 关键字与 Goroutine#

基础用法#

Goroutine 是 Golang 最强大的特性之一,Yak 完美继承了这一特性。

Yak 脚本与 Golang 的 Go 的作用都是相同的,但是需要注意一点的是,go 关键字可以用来启动 yak 的闭包函数

在 Golang 中,我们启动一个 Goroutine 通过以下操作启动

go func(){    // ...do some codes}()

在 yak 中,我们不仅兼容了上述写法,我们执行如下命令,都是等效的

//  goroutine 启动来函数异步调用go fn(){}()go func(){}() // 兼容 Go 的写法go def(){}()  // 兼容 Python 定义方法的关键字
// 定义闭包,执行 Goroutinego fn{/*do sth*/}go func{/*do sth*/}go def{/*do sth*/}

并发控制用例#

一个比较复杂的例子:

wg = sync.NewWaitGroup()wg.Add(2)
go func {    defer wg.Done()    println("in goroutine1")}
go func {    defer wg.Done()    println("in goroutine2")}
wg.Wait()

我们执行上述代码,程序将会等待直到两个 Goroutine 都执行完才会退出,这属于比较经典的并发案例。

Defer 机制与 Golang 的 Defer#

基本和 Golang 的 defer 用法类似

但是,由于匿名函数存在,所以 yak defer 常见有两种写法:

defer fn() {} ()defer fn{}

值得注意的是:

在一个细节上 yak 的 defer 和 Golang 处理并不一致,那就是 defer 表达式中的变量值。

在 Golang 中,所有 defer 引用的变量均在 defer 语句时刻固定下来后面任何修改均不影响 defer 语句的行为,但 yak 是会受到影响的,我们可以观察如下案例:

f = {"ccc": 1}dump(f.ccc)defer func{    println("准备开始执行 defer func")    println(f.ccc)    // 等到执行这里的时候,就会报错}       println("设置 f 变量为空")f = nil               // 在这里设置 f 为空

例如,假设你在 defer 之后,调用 f = nil 把 f 变量改为 nil,那么后面执行 f.Close() 时就会发生错误。

上述代码段执行结果为:

设置 f 变量为空准备开始执行 defer func[ERRO] 2021-05-26 00:27:59 +0800 [default:yak.go:100] reflect: call of reflect.Value.MethodByName on zero Value
defer fn {    dump(11111)}
defer fn(){    dump(111)}()
// 输出结果为// (int) 111// (int) 11111

流程控制#

if/elif/else 条件分支#

if expr {
} elif expr2 {
} else {
}

switch/case 语句#

yak switch 语句和 Golang 的有共同点也有不同点;

  1. yak 的 swtich 没有 break / fallthrough 的特性支持
  2. yak switch 后的表达式只能是表达式,不能像 Golang 一样承载赋值语句; 表达式
// switch expr1 {case: expr2; default}a = 5switch a - 3 {case 2:  println("case first")case 3:  println("case second")}
switch {case true:  println("true case ")case false:  println("false case")}
注意差异 yak 的 switch/case/default 只能算简易版的 if/elif/else,并不支持 fallthroughbreak

for 语句 与 for range 语句#

for 类似 Golang 的 for 语句,同时支持 continue break 这些常规操作。

无限循环#

for { // 无限循环,需要在中间 break 或 return 结束    ...}
for booleanExpr { // 类似很多语言的 while 循环    ...}

基础使用#

/*for initExpr; conditionExpr; stepExpr {    ...}*/
for i = 0; i < 10; i ++ {    println("element: ", i)}
/*OUTPUT:
element:  0element:  1element:  2element:  3element:  4element:  5element:  6element:  7element:  8element:  9*/

这种用法我想大家都很熟悉了,我们不需要过多叙述。

for range 来遍历一个 slice / list#

for range 是 Golang 特有的形式,yak 对这种形式进行了保留

// 声明一个最基础的 slice / lista = [1,2,3,4]
// 遍历这个 slice / list,第一个参数为 index,第二个参数为具体的 slice 中的元素for index, element = range a {    println(str.f("a[%v]: %v", index, element))}println("-----------------")
// 可以只去 index,continue 是for index = range a {    println(str.f("a[%v]", index))    continue}println("-----------------")for _, element = range a {    println(str.f("first element: %v", element))    break}

for range 来遍历一个 map / dict#

b = {"abc": "123", "bcd": "bcd", "cde": 512}for key, value = range b {    println(str.f("b[%v]: %v", key, value))}
/**OUTPUT:
b[abc]: 123b[bcd]: bcdb[cde]: 512*/

上述脚本很容易猜到,结果如下

a[0]: 1a[1]: 2a[2]: 3a[3]: 4-----------------a[0]a[1]a[2]a[3]-----------------current element: 1

for range 同样可以操作 chan#

ch := make(chan var, 2)ch <- 1ch <- 2close(ch)
for result = range ch {    println("fetch chan var [ch] element: ", result)}
/*OUTPUT:
fetch chan var [ch] element:  1fetch chan var [ch] element:  2*/

模块化/多文件编程#

在模块化与多文件编程中,我们通常需要根据相对位置定位文件或者资源目录

为此我们准备有三个常见全局变量方便用户操作

  1. YAK_MAIN: boolean 类型,如果为 false 说明这个文件是主文件,通过动态引入的文件将会为 false
  2. YAK_FILENAME: 当前执行的脚本文件的具体文件名
  3. YAK_DIR: 当前脚本文件所在的路径位置

使用 YAK_MAIN:等价于 Python 中的 __main__#

如果你调用一个 yak 脚本,通过 yak [your-yak-script].yak 来调用,那么脚本中的 YAK_MAIN 将会设置为 true

相反,如果是通过 dyn 中的包来调用,那么,YAK_MAIN 的结果则会是 false

所以我们在看下面代码:

main.yak
func aaa(caller) {   println("aaa is called by", caller)}
if YAK_MAIN {   println("i am in main.... block")   aaa("main")}

当我们调用 yak main.yak 的时候,界面展示

i am in main.... blockaaa is called by main

我们发现,我们定义的函数执行了,YAK_MAIN 的值为 true

作为对比,我们在同一个目录下,编写一个 foo.yak,内容如下

foo.yak
res, err := dyn.LoadVarFromFile("main.yak", "aaa")die(err)
res[0].Value("foo.yak")

我们执行 yak foo.yak 之后,发现屏幕打印出:

aaa is called by foo.yak

我们发现:

if YAK_MAIN {   println("i am in main.... block")   aaa("main")}

这段代码并没有被执行,这是因为 main.yak 第二次执行的时候,并不是主函数。所以 YAK_MAINfalse

内置函数 import:加载另外的脚本中的变量。#

定义#

func import(modName: string, varName: string) (*yak.yakVariable, error)

note

*yak.yakVariable 定义如下:

type palm/common/yak.(yakVariable) struct {  Fields(可用字段):       FilePath: string        YakMod: string        Value: interface {}    StructMethods(结构方法/函数):   PtrStructMethods(指针结构方法/函数):       func Callable() return(bool) }

我们执行完上述函数之后,将会把一个本地 modName 单独加载到我们新的脚本中,并把变量名为 varName 的变量导出。

如果执行失败,返回值将会为 空+err

Quick Start#

我们先创建一个 a1.yak 脚本

a1.yak
func poc() {   println("I am in `poc` function....")}
if YAK_MAIN {   println("poc function is called by a1")   poc()}

然后我们创建一个 main.yak 脚本

main.yak
pocVar, err = import("a1", "poc")if err != nil {    die(err)}
if YAK_MAIN {   println("poc function is called by main")   pocVar.Value()}

如果我们单独执行 main.yak 我们发现输出为

yak main.yak
poc function is called by mainI am in `poc` function....

如果我们单独执行 a1.yak 我们发现输出为:

yak a1.yak
poc function is called by a1I am in `poc` function....

显然的我们从 main.yak 调用到了 a1.yak 中的函数(实际上是变量)。非常简单实用!

include 像 PHP include 另一个脚本#

include 只有在脚本执行之前进行执行,本质相当于把一个新的文件 "复制" 一个当前脚本中执行了。

作为对比,我们继续使用上一节的 main.yak 函数。通过 include 来执行。

foo2.yak
include "main.yak"
if YAK_MAIN {    aaa("foo2.yak")}

我们执行上述代码之后,发现结果如下

i am in main.... blockaaa is called by mainaaa is called by foo2.yak

main.yak 中的 if YAK_MAIN{} 分支下的内容被执行了。