mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-07 14:25:38 +00:00
Add support for Lua input (#1810)
This commit is contained in:
parent
ee900ec997
commit
5fa41624c9
@ -130,6 +130,8 @@ func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {
|
|||||||
|
|
||||||
func createDecoder(format yqlib.InputFormat, evaluateTogether bool) (yqlib.Decoder, error) {
|
func createDecoder(format yqlib.InputFormat, evaluateTogether bool) (yqlib.Decoder, error) {
|
||||||
switch format {
|
switch format {
|
||||||
|
case yqlib.LuaInputFormat:
|
||||||
|
return yqlib.NewLuaDecoder(yqlib.ConfiguredLuaPreferences), nil
|
||||||
case yqlib.XMLInputFormat:
|
case yqlib.XMLInputFormat:
|
||||||
return yqlib.NewXMLDecoder(yqlib.ConfiguredXMLPreferences), nil
|
return yqlib.NewXMLDecoder(yqlib.ConfiguredXMLPreferences), nil
|
||||||
case yqlib.PropertiesInputFormat:
|
case yqlib.PropertiesInputFormat:
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -15,6 +15,7 @@ require (
|
|||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/yuin/gopher-lua v1.1.0
|
||||||
golang.org/x/net v0.15.0
|
golang.org/x/net v0.15.0
|
||||||
golang.org/x/text v0.13.0
|
golang.org/x/text v0.13.0
|
||||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -50,6 +50,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||||
|
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const (
|
|||||||
TSVObjectInputFormat
|
TSVObjectInputFormat
|
||||||
TomlInputFormat
|
TomlInputFormat
|
||||||
UriInputFormat
|
UriInputFormat
|
||||||
|
LuaInputFormat
|
||||||
)
|
)
|
||||||
|
|
||||||
type Decoder interface {
|
type Decoder interface {
|
||||||
@ -41,6 +42,8 @@ func InputFormatFromString(format string) (InputFormat, error) {
|
|||||||
return TSVObjectInputFormat, nil
|
return TSVObjectInputFormat, nil
|
||||||
case "toml":
|
case "toml":
|
||||||
return TomlInputFormat, nil
|
return TomlInputFormat, nil
|
||||||
|
case "lua", "l":
|
||||||
|
return LuaInputFormat, nil
|
||||||
default:
|
default:
|
||||||
return 0, fmt.Errorf("unknown format '%v' please use [yaml|json|props|csv|tsv|xml|toml]", format)
|
return 0, fmt.Errorf("unknown format '%v' please use [yaml|json|props|csv|tsv|xml|toml]", format)
|
||||||
}
|
}
|
||||||
|
|||||||
167
pkg/yqlib/decoder_lua.go
Normal file
167
pkg/yqlib/decoder_lua.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package yqlib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type luaDecoder struct {
|
||||||
|
reader io.Reader
|
||||||
|
finished bool
|
||||||
|
prefs LuaPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLuaDecoder(prefs LuaPreferences) Decoder {
|
||||||
|
return &luaDecoder{
|
||||||
|
prefs: prefs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *luaDecoder) Init(reader io.Reader) error {
|
||||||
|
dec.reader = reader
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *luaDecoder) convertToYamlNode(ls *lua.LState, lv lua.LValue) *yaml.Node {
|
||||||
|
switch lv.Type() {
|
||||||
|
case lua.LTNil:
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!null",
|
||||||
|
Value: "",
|
||||||
|
}
|
||||||
|
case lua.LTBool:
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!bool",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
case lua.LTNumber:
|
||||||
|
n := float64(lua.LVAsNumber(lv))
|
||||||
|
// various special case floats
|
||||||
|
if math.IsNaN(n) {
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!float",
|
||||||
|
Value: ".nan",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if math.IsInf(n, 1) {
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!float",
|
||||||
|
Value: ".inf",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if math.IsInf(n, -1) {
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!float",
|
||||||
|
Value: "-.inf",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// does it look like an integer?
|
||||||
|
if n == float64(int(n)) {
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!int",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!float",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
case lua.LTString:
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!str",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
case lua.LTFunction:
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "tag:lua.org,2006,function",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
case lua.LTTable:
|
||||||
|
// Simultaneously create a sequence and a map, pick which one to return
|
||||||
|
// based on whether all keys were consecutive integers
|
||||||
|
i := 1
|
||||||
|
yaml_sequence := &yaml.Node{
|
||||||
|
Kind: yaml.SequenceNode,
|
||||||
|
Tag: "!!seq",
|
||||||
|
}
|
||||||
|
yaml_map := &yaml.Node{
|
||||||
|
Kind: yaml.MappingNode,
|
||||||
|
Tag: "!!map",
|
||||||
|
}
|
||||||
|
t := lv.(*lua.LTable)
|
||||||
|
k, v := ls.Next(t, lua.LNil)
|
||||||
|
for k != lua.LNil {
|
||||||
|
if ki, ok := k.(lua.LNumber); i != 0 && ok && math.Mod(float64(ki), 1) == 0 && int(ki) == i {
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
yaml_map.Content = append(yaml_map.Content, dec.convertToYamlNode(ls, k))
|
||||||
|
yv := dec.convertToYamlNode(ls, v)
|
||||||
|
yaml_map.Content = append(yaml_map.Content, yv)
|
||||||
|
if i != 0 {
|
||||||
|
yaml_sequence.Content = append(yaml_sequence.Content, yv)
|
||||||
|
}
|
||||||
|
k, v = ls.Next(t, k)
|
||||||
|
}
|
||||||
|
if i != 0 {
|
||||||
|
return yaml_sequence
|
||||||
|
}
|
||||||
|
return yaml_map
|
||||||
|
default:
|
||||||
|
return &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
LineComment: fmt.Sprintf("Unhandled Lua type: %s", lv.Type().String()),
|
||||||
|
Tag: "!!null",
|
||||||
|
Value: lv.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *luaDecoder) decideTopLevelNode(ls *lua.LState) *yaml.Node {
|
||||||
|
if ls.GetTop() == 0 {
|
||||||
|
// no items were explicitly returned, encode the globals table instead
|
||||||
|
return dec.convertToYamlNode(ls, ls.Get(lua.GlobalsIndex))
|
||||||
|
}
|
||||||
|
return dec.convertToYamlNode(ls, ls.Get(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *luaDecoder) Decode() (*CandidateNode, error) {
|
||||||
|
if dec.finished {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
ls := lua.NewState(lua.Options{SkipOpenLibs: true})
|
||||||
|
defer ls.Close()
|
||||||
|
fn, err := ls.Load(dec.reader, "@input")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ls.Push(fn)
|
||||||
|
err = ls.PCall(0, lua.MultRet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
firstNode := dec.decideTopLevelNode(ls)
|
||||||
|
dec.finished = true
|
||||||
|
return &CandidateNode{
|
||||||
|
Node: &yaml.Node{
|
||||||
|
Kind: yaml.DocumentNode,
|
||||||
|
Content: []*yaml.Node{firstNode},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@ -1,5 +1,33 @@
|
|||||||
|
|
||||||
## Basic example
|
## Basic input example
|
||||||
|
Given a sample.lua file of:
|
||||||
|
```lua
|
||||||
|
return {
|
||||||
|
["country"] = "Australia"; -- this place
|
||||||
|
["cities"] = {
|
||||||
|
"Sydney",
|
||||||
|
"Melbourne",
|
||||||
|
"Brisbane",
|
||||||
|
"Perth",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```bash
|
||||||
|
yq -oy '.' sample.lua
|
||||||
|
```
|
||||||
|
will output
|
||||||
|
```yaml
|
||||||
|
country: Australia
|
||||||
|
cities:
|
||||||
|
- Sydney
|
||||||
|
- Melbourne
|
||||||
|
- Brisbane
|
||||||
|
- Perth
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic output example
|
||||||
Given a sample.yml file of:
|
Given a sample.yml file of:
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
|
|||||||
@ -10,7 +10,27 @@ import (
|
|||||||
|
|
||||||
var luaScenarios = []formatScenario{
|
var luaScenarios = []formatScenario{
|
||||||
{
|
{
|
||||||
description: "Basic example",
|
description: "Basic input example",
|
||||||
|
input: `return {
|
||||||
|
["country"] = "Australia"; -- this place
|
||||||
|
["cities"] = {
|
||||||
|
"Sydney",
|
||||||
|
"Melbourne",
|
||||||
|
"Brisbane",
|
||||||
|
"Perth",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
expected: `country: Australia
|
||||||
|
cities:
|
||||||
|
- Sydney
|
||||||
|
- Melbourne
|
||||||
|
- Brisbane
|
||||||
|
- Perth
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Basic output example",
|
||||||
scenarioType: "encode",
|
scenarioType: "encode",
|
||||||
input: `---
|
input: `---
|
||||||
country: Australia # this place
|
country: Australia # this place
|
||||||
@ -28,6 +48,31 @@ cities:
|
|||||||
"Perth",
|
"Perth",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Basic roundtrip",
|
||||||
|
skipDoc: true,
|
||||||
|
scenarioType: "roundtrip",
|
||||||
|
input: `return {
|
||||||
|
["country"] = "Australia"; -- this place
|
||||||
|
["cities"] = {
|
||||||
|
"Sydney",
|
||||||
|
"Melbourne",
|
||||||
|
"Brisbane",
|
||||||
|
"Perth",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
expected: `return {
|
||||||
|
["country"] = "Australia";
|
||||||
|
["cities"] = {
|
||||||
|
"Sydney",
|
||||||
|
"Melbourne",
|
||||||
|
"Brisbane",
|
||||||
|
"Perth",
|
||||||
|
};
|
||||||
|
};
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -168,8 +213,12 @@ numbers:
|
|||||||
|
|
||||||
func testLuaScenario(t *testing.T, s formatScenario) {
|
func testLuaScenario(t *testing.T, s formatScenario) {
|
||||||
switch s.scenarioType {
|
switch s.scenarioType {
|
||||||
|
case "", "decode":
|
||||||
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewLuaDecoder(ConfiguredLuaPreferences), NewYamlEncoder(4, false, ConfiguredYamlPreferences)), s.description)
|
||||||
case "encode":
|
case "encode":
|
||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(ConfiguredLuaPreferences)), s.description)
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(ConfiguredLuaPreferences)), s.description)
|
||||||
|
case "roundtrip":
|
||||||
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewLuaDecoder(ConfiguredLuaPreferences), NewLuaEncoder(ConfiguredLuaPreferences)), s.description)
|
||||||
case "unquoted-encode":
|
case "unquoted-encode":
|
||||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(LuaPreferences{
|
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(LuaPreferences{
|
||||||
DocPrefix: "return ",
|
DocPrefix: "return ",
|
||||||
@ -196,13 +245,39 @@ func documentLuaScenario(t *testing.T, w *bufio.Writer, i interface{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch s.scenarioType {
|
switch s.scenarioType {
|
||||||
|
case "", "decode":
|
||||||
|
documentLuaDecodeScenario(w, s)
|
||||||
case "encode", "unquoted-encode", "globals-encode":
|
case "encode", "unquoted-encode", "globals-encode":
|
||||||
documentLuaEncodeScenario(w, s)
|
documentLuaEncodeScenario(w, s)
|
||||||
|
case "roundtrip":
|
||||||
|
documentLuaRoundTripScenario(w, s)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func documentLuaDecodeScenario(w *bufio.Writer, s formatScenario) {
|
||||||
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||||
|
|
||||||
|
if s.subdescription != "" {
|
||||||
|
writeOrPanic(w, s.subdescription)
|
||||||
|
writeOrPanic(w, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOrPanic(w, "Given a sample.lua file of:\n")
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```lua\n%v\n```\n", s.input))
|
||||||
|
|
||||||
|
writeOrPanic(w, "then\n")
|
||||||
|
expression := s.expression
|
||||||
|
if expression == "" {
|
||||||
|
expression = "."
|
||||||
|
}
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```bash\nyq -oy '%v' sample.lua\n```\n", expression))
|
||||||
|
writeOrPanic(w, "will output\n")
|
||||||
|
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewLuaDecoder(ConfiguredLuaPreferences), NewYamlEncoder(2, false, ConfiguredYamlPreferences))))
|
||||||
|
}
|
||||||
|
|
||||||
func documentLuaEncodeScenario(w *bufio.Writer, s formatScenario) {
|
func documentLuaEncodeScenario(w *bufio.Writer, s formatScenario) {
|
||||||
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||||
|
|
||||||
@ -245,6 +320,24 @@ func documentLuaEncodeScenario(w *bufio.Writer, s formatScenario) {
|
|||||||
writeOrPanic(w, fmt.Sprintf("```lua\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(prefs))))
|
writeOrPanic(w, fmt.Sprintf("```lua\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewLuaEncoder(prefs))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func documentLuaRoundTripScenario(w *bufio.Writer, s formatScenario) {
|
||||||
|
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
|
||||||
|
|
||||||
|
if s.subdescription != "" {
|
||||||
|
writeOrPanic(w, s.subdescription)
|
||||||
|
writeOrPanic(w, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOrPanic(w, "Given a sample.lua file of:\n")
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```lua\n%v\n```\n", s.input))
|
||||||
|
|
||||||
|
writeOrPanic(w, "then\n")
|
||||||
|
writeOrPanic(w, "```bash\nyq '.' sample.lua\n```\n")
|
||||||
|
writeOrPanic(w, "will output\n")
|
||||||
|
|
||||||
|
writeOrPanic(w, fmt.Sprintf("```lua\n%v```\n\n", mustProcessFormatScenario(s, NewLuaDecoder(ConfiguredLuaPreferences), NewLuaEncoder(ConfiguredLuaPreferences))))
|
||||||
|
}
|
||||||
|
|
||||||
func TestLuaScenarios(t *testing.T) {
|
func TestLuaScenarios(t *testing.T) {
|
||||||
for _, tt := range luaScenarios {
|
for _, tt := range luaScenarios {
|
||||||
testLuaScenario(t, tt)
|
testLuaScenario(t, tt)
|
||||||
|
|||||||
@ -126,6 +126,7 @@ Lexer
|
|||||||
libdistro
|
libdistro
|
||||||
lindex
|
lindex
|
||||||
linecomment
|
linecomment
|
||||||
|
LVAs
|
||||||
magiconair
|
magiconair
|
||||||
mapvalues
|
mapvalues
|
||||||
Mier
|
Mier
|
||||||
@ -135,6 +136,7 @@ minishift
|
|||||||
mipsle
|
mipsle
|
||||||
mitchellh
|
mitchellh
|
||||||
mktemp
|
mktemp
|
||||||
|
Mult
|
||||||
multidoc
|
multidoc
|
||||||
multimaint
|
multimaint
|
||||||
myenv
|
myenv
|
||||||
@ -244,4 +246,5 @@ xmld
|
|||||||
xyzzy
|
xyzzy
|
||||||
yamld
|
yamld
|
||||||
yqlib
|
yqlib
|
||||||
|
yuin
|
||||||
zabbix
|
zabbix
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user