diff --git a/README.md b/README.md index b47bfe8..45e657d 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,26 @@ SELECT yaml_to_json("hello: world") SELECT json_to_yaml('{"hello":"world"}') -- hello: world ``` +## cmd + +`cmd` is a scalar function that expects a single argument (a bash command string) and it will return the results + +```sql +SELECT cmd("echo 'Hello, World'") +-- Hello, World +``` + +## cmd_table + +`cmd_table` is a module that takes in a bash string command and an optional delimiter(default "\n") returning a row for each line + +| Column | Type | +|-----------------|----------| +| line_no | INT | +| contents | TEXT | + +```sql +SELECT cmd_table("echo 'Hello, World'",' ') +-- 1 , Hello, +-- 2 , World +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 6ca50df..6080ab7 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.16 require ( github.com/augmentable-dev/vtab v0.0.0-20210328214525-c302d68997b8 github.com/jmoiron/sqlx v1.3.4 + github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-sqlite3 v1.14.6 go.riyazali.net/sqlite v0.0.0-20210326190148-448ec1ab2454 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index f3d2146..80ed6af 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,11 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= @@ -14,8 +19,9 @@ github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRU github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= go.riyazali.net/sqlite v0.0.0-20210326190148-448ec1ab2454 h1:hhX5XmBE7RzVDdriWQMjQ1Gfx4JE2oC8IGe/YCNEaFQ= go.riyazali.net/sqlite v0.0.0-20210326190148-448ec1ab2454/go.mod h1:NNie4T5ol2qbMg7kw1QkXDrcgTeXQgbGxEhn1m4nBu0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/cmd_read/cmd_read.go b/internal/cmd_read/cmd_read.go new file mode 100644 index 0000000..2b4bd3d --- /dev/null +++ b/internal/cmd_read/cmd_read.go @@ -0,0 +1,35 @@ +package cmd_read + +import ( + "os/exec" + + "go.riyazali.net/sqlite" +) + +type cmdRead struct{} + +func (f *cmdRead) Args() int { return 1 } +func (f *cmdRead) Deterministic() bool { return false } +func (f *cmdRead) Apply(ctx *sqlite.Context, values ...sqlite.Value) { + var ( + command string + err error + ) + + if len(values) > 0 { + command = values[0].Text() + } + cmd := exec.Command("bash", "-c", command) + stdout, err := cmd.Output() + + if err != nil { + ctx.ResultError(err) + } + + ctx.ResultText(string(stdout)) +} + +// New returns a sqlite function for reading the contents of a cmd +func New() sqlite.Function { + return &cmdRead{} +} diff --git a/internal/cmd_read/cmd_read_table.go b/internal/cmd_read/cmd_read_table.go new file mode 100644 index 0000000..ea19750 --- /dev/null +++ b/internal/cmd_read/cmd_read_table.go @@ -0,0 +1,75 @@ +package cmd_read + +import ( + "fmt" + "io" + "os/exec" + "strings" + + "github.com/augmentable-dev/vtab" + "go.riyazali.net/sqlite" +) + +type iterCommands struct { + command string + delimiter string + contents []string + index int +} + +func (i *iterCommands) Column(c int) (interface{}, error) { + switch c { + case 0: + return i.command, nil + case 1: + return i.delimiter, nil + case 2: + return i.index, nil + case 3: + return i.contents[i.index], nil + } + return nil, fmt.Errorf("unknown column") +} +func (i *iterCommands) Next() (vtab.Row, error) { + i.index += 1 + if i.index >= len(i.contents) { + return nil, io.EOF + } + + return i, nil +} + +var commandCols = []vtab.Column{ + {Name: "command", Type: sqlite.SQLITE_TEXT, NotNull: true, Hidden: true, Filters: []sqlite.ConstraintOp{sqlite.INDEX_CONSTRAINT_EQ}}, + {Name: "delimiter", Type: sqlite.SQLITE_TEXT, NotNull: true, Hidden: true, Filters: []sqlite.ConstraintOp{sqlite.INDEX_CONSTRAINT_EQ}}, + {Name: "line_no", Type: sqlite.SQLITE_INTEGER}, + {Name: "results", Type: sqlite.SQLITE_TEXT}, +} + +func NewCommandModule() sqlite.Module { + return vtab.NewTableFunc("command", commandCols, func(constraints []vtab.Constraint) (vtab.Iterator, error) { + var command, delimiter string + for _, constraint := range constraints { + if constraint.Op == sqlite.INDEX_CONSTRAINT_EQ { + switch constraint.ColIndex { + case 0: + command = constraint.Value.Text() + case 1: + delimiter = constraint.Value.Text() + } + } + } + + if delimiter == "" { + delimiter = "\n" + } + cmd := exec.Command("bash", "-c", command) + stdout, err := cmd.Output() + if err != nil { + return nil, err + } + contents := strings.Split(string(stdout), delimiter) + iter := &iterCommands{command, delimiter, contents, -1} + return iter, nil + }) +} diff --git a/internal/cmd_read/cmd_read_table_test.go b/internal/cmd_read/cmd_read_table_test.go new file mode 100644 index 0000000..019139a --- /dev/null +++ b/internal/cmd_read/cmd_read_table_test.go @@ -0,0 +1,77 @@ +package cmd_read + +import ( + "database/sql" + "strings" + "testing" + + _ "github.com/augmentable-dev/flite/internal/sqlite" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "go.riyazali.net/sqlite" +) + +func RowContent(rows *sql.Rows) (colCount int, contents [][]string, err error) { + columns, err := rows.Columns() + if err != nil { + return colCount, nil, err + } + + colCount = len(columns) + + pointers := make([]interface{}, len(columns)) + container := make([]sql.NullString, len(columns)) + + for i := range pointers { + pointers[i] = &container[i] + } + + for rows.Next() { + err = rows.Scan(pointers...) + if err != nil { + return colCount, nil, err + } + + r := make([]string, len(columns)) + for i, c := range container { + if c.Valid { + r[i] = c.String + } else { + r[i] = "NULL" + } + } + contents = append(contents, r) + } + return colCount, contents, rows.Err() + +} + +func TestReadTableCommand(t *testing.T) { + commandToExec := "echo Hello World" + checkOutput := strings.Split("Hello World", " ") + sqlite.Register(func(api *sqlite.ExtensionApi) (sqlite.ErrorCode, error) { + if err := api.CreateModule("cmd_table", NewCommandModule(), + sqlite.EponymousOnly(true), sqlite.ReadOnly(true)); err != nil { + return sqlite.SQLITE_ERROR, err + } + return sqlite.SQLITE_OK, nil + }) + db, err := sqlx.Open("sqlite3", ":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + rows, err := db.Query("SELECT * from cmd_table($1 ,' ')", commandToExec) + if err != nil { + t.Fatal(err) + } + _, content, err := RowContent(rows) + for i := 0; i < len(content); i++ { + if strings.TrimSpace(content[i][1]) != strings.TrimSpace(checkOutput[i]) { + t.Fatalf("expected response: %s, got: %s", checkOutput[i], content[i][1]) + } + } + if rows.Err() != nil { + t.Fatal(err) + } +} diff --git a/internal/cmd_read/cmd_read_test.go b/internal/cmd_read/cmd_read_test.go new file mode 100644 index 0000000..c65a83e --- /dev/null +++ b/internal/cmd_read/cmd_read_test.go @@ -0,0 +1,39 @@ +package cmd_read + +import ( + "strings" + "testing" + + _ "github.com/augmentable-dev/flite/internal/sqlite" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "go.riyazali.net/sqlite" +) + +func TestReadCommand(t *testing.T) { + commandToExec := "echo Hello World" + sqlite.Register(func(api *sqlite.ExtensionApi) (sqlite.ErrorCode, error) { + if err := api.CreateFunction("cmd", New()); err != nil { + return sqlite.SQLITE_ERROR, err + } + return sqlite.SQLITE_OK, nil + }) + db, err := sqlx.Open("sqlite3", ":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + row := db.QueryRow("SELECT cmd($1)", commandToExec) + err = row.Err() + if err != nil { + t.Fatal(err) + } + var res string + err = row.Scan(&res) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(res) != "Hello World" { + t.Fatalf("expected response: %s, got: %s", "Hello World", res) + } +} diff --git a/pkg/ext/ext.go b/pkg/ext/ext.go index d4a0919..4863beb 100644 --- a/pkg/ext/ext.go +++ b/pkg/ext/ext.go @@ -1,10 +1,12 @@ package ext import ( + "github.com/augmentable-dev/flite/internal/cmd_read" "github.com/augmentable-dev/flite/internal/file_read" "github.com/augmentable-dev/flite/internal/file_split" "github.com/augmentable-dev/flite/internal/http" "github.com/augmentable-dev/flite/internal/yaml" + _ "github.com/mattn/go-sqlite3" "go.riyazali.net/sqlite" ) @@ -15,6 +17,10 @@ func init() { sqlite.EponymousOnly(true), sqlite.ReadOnly(true)); err != nil { return sqlite.SQLITE_ERROR, err } + if err := api.CreateModule("cmd_table", cmd_read.NewCommandModule(), + sqlite.EponymousOnly(true), sqlite.ReadOnly(true)); err != nil { + return sqlite.SQLITE_ERROR, err + } if err := api.CreateFunction("file_read", file_read.New()); err != nil { return sqlite.SQLITE_ERROR, err @@ -31,6 +37,9 @@ func init() { if err := api.CreateFunction("http_get", http.NewHTTPGet()); err != nil { return sqlite.SQLITE_ERROR, err } + if err := api.CreateFunction("cmd_read", cmd_read.New()); err != nil { + return sqlite.SQLITE_ERROR, err + } return sqlite.SQLITE_OK, nil })