Skip to content

Commit 2a58253

Browse files
committed
feat: init
0 parents  commit 2a58253

34 files changed

+4070
-0
lines changed

LICENSE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2024 b4fun
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# script-contextual
2+
3+
This library provides experimental "contextual" support for github.com/bitfield/script .
4+
5+
## TODO
6+
7+
- [ ] copy tests from upstream
8+
9+
## License
10+
11+
MIT

generator/go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module generator
2+
3+
go 1.22.3
4+
5+
require github.com/bitfield/script v0.22.1
6+
7+
require (
8+
github.com/itchyny/gojq v0.12.13 // indirect
9+
github.com/itchyny/timefmt-go v0.1.5 // indirect
10+
mvdan.cc/sh/v3 v3.7.0 // indirect
11+
)

generator/go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/bitfield/script v0.22.1 h1:DphxoC5ssYciwd0ZS+N0Xae46geAD/0mVWh6a2NUxM4=
2+
github.com/bitfield/script v0.22.1/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI=
3+
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
4+
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
5+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
6+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7+
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
8+
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
9+
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
10+
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
11+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
12+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
13+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
14+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
15+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
16+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
17+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
18+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19+
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
20+
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
21+
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
22+
mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=

generator/main.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"go/ast"
8+
"go/parser"
9+
"go/token"
10+
"reflect"
11+
"sort"
12+
"strings"
13+
"text/template"
14+
15+
"github.com/bitfield/script"
16+
)
17+
18+
func pipeFromBitfield() map[string]reflect.Method {
19+
p := script.NewPipe()
20+
pipeType := reflect.ValueOf(p).Type()
21+
methods := make(map[string]reflect.Method)
22+
for i := 0; i < pipeType.NumMethod(); i++ {
23+
method := pipeType.Method(i)
24+
// take method returning *Pipe
25+
if method.Func.Type().NumOut() != 1 {
26+
continue
27+
}
28+
if method.Func.Type().Out(0) != reflect.TypeOf(p) {
29+
continue
30+
}
31+
methods[method.Name] = method
32+
}
33+
return methods
34+
}
35+
36+
func pipeFromSourceFile(file string) (map[string]struct{}, error) {
37+
// Create a new file set
38+
fset := token.NewFileSet()
39+
40+
// Parse the Go source file
41+
node, err := parser.ParseFile(fset, file, nil, parser.AllErrors)
42+
if err != nil {
43+
return nil, fmt.Errorf("error parsing file: %w", err)
44+
}
45+
46+
rv := make(map[string]struct{})
47+
48+
for _, decl := range node.Decls {
49+
fn, ok := decl.(*ast.FuncDecl)
50+
if !ok || fn.Recv == nil {
51+
continue
52+
}
53+
// check if the receiver is *Pipe
54+
if len(fn.Recv.List) != 1 {
55+
continue
56+
}
57+
if _, ok := fn.Recv.List[0].Type.(*ast.StarExpr); !ok {
58+
continue
59+
}
60+
if sel, ok := fn.Recv.List[0].Type.(*ast.StarExpr).X.(*ast.SelectorExpr); ok {
61+
if sel.Sel.Name != "Pipe" {
62+
continue
63+
}
64+
}
65+
if !isPublic(fn.Name.Name) {
66+
continue
67+
}
68+
rv[fn.Name.Name] = struct{}{}
69+
}
70+
71+
return rv, nil
72+
}
73+
74+
func isPublic(name string) bool {
75+
return strings.ToUpper(name[:1]) == name[:1]
76+
}
77+
78+
var pipeMethodTemplate = template.Must(
79+
template.New("pipeMethodTemplate").
80+
Parse(`
81+
func (p *Pipe) {{.Name}}({{.Params}}) *Pipe {
82+
p.Pipe = p.Pipe.{{.Name}}({{.Args}})
83+
return p
84+
}
85+
`))
86+
87+
var generatedTemplate = template.Must(
88+
template.New("generatedTemplate").
89+
Parse(`// Code generated by script-contextual/generator DO NOT EDIT.
90+
package script
91+
92+
import (
93+
"io"
94+
"strings"
95+
"regexp"
96+
"net/http"
97+
)
98+
99+
{{.Methods}}
100+
`))
101+
102+
var flagSourceFile string
103+
104+
func init() {
105+
flag.StringVar(&flagSourceFile, "source", "", "source file to generate from")
106+
}
107+
108+
func main() {
109+
flag.Parse()
110+
if flagSourceFile == "" {
111+
panic("source file is required")
112+
}
113+
114+
pipeMethodsFromBitfield := pipeFromBitfield()
115+
pipeMethodsFromSource, err := pipeFromSourceFile(flagSourceFile)
116+
if err != nil {
117+
panic(err)
118+
}
119+
120+
methodNamesSorted := make([]string, 0, len(pipeMethodsFromBitfield))
121+
for methodName := range pipeMethodsFromBitfield {
122+
methodNamesSorted = append(methodNamesSorted, methodName)
123+
}
124+
sort.Strings(methodNamesSorted)
125+
126+
var methodsCode []string
127+
for _, methodName := range methodNamesSorted {
128+
if _, ok := pipeMethodsFromSource[methodName]; ok {
129+
continue
130+
}
131+
132+
method := pipeMethodsFromBitfield[methodName]
133+
methodFuncType := method.Func.Type()
134+
var params []string
135+
var args []string
136+
// 0 is the method receiver
137+
for i := 1; i < methodFuncType.NumIn(); i++ {
138+
arg := methodFuncType.In(i)
139+
argName := fmt.Sprintf("a%d", i)
140+
if methodFuncType.IsVariadic() && i == methodFuncType.NumIn()-1 {
141+
params = append(params, fmt.Sprintf("%s ...%s", argName, arg.Elem().String()))
142+
args = append(args, fmt.Sprintf("%s...", argName))
143+
} else {
144+
params = append(params, fmt.Sprintf("%s %s", argName, arg.String()))
145+
args = append(args, argName)
146+
}
147+
}
148+
149+
// convert method to template payload:
150+
// - Name: method name
151+
// - Params: method typed parameters
152+
// - Args: method parameters
153+
payload := map[string]string{
154+
"Name": methodName,
155+
"Params": strings.Join(params, ", "),
156+
"Args": strings.Join(args, ", "),
157+
}
158+
b := new(bytes.Buffer)
159+
if err := pipeMethodTemplate.Execute(b, payload); err != nil {
160+
panic(err)
161+
}
162+
163+
methodsCode = append(methodsCode, b.String())
164+
}
165+
166+
payload := map[string]string{
167+
"Methods": strings.Join(methodsCode, "\n"),
168+
}
169+
generatedCode := new(bytes.Buffer)
170+
if err := generatedTemplate.Execute(generatedCode, payload); err != nil {
171+
panic(err)
172+
}
173+
174+
generatedCodeFormatted, err := script.
175+
Echo(generatedCode.String()).
176+
Exec("gofmt").
177+
// Tee(os.Stderr).
178+
String()
179+
if err != nil {
180+
panic(err)
181+
}
182+
183+
fmt.Println(generatedCodeFormatted)
184+
}

generator/run.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
3+
CUR_DIR=$(cd $(dirname $0); pwd)
4+
SCRIPT_DIR="${CUR_DIR}/../script"
5+
6+
pushd "${CUR_DIR}" > /dev/null
7+
go run main.go -source "${SCRIPT_DIR}/contextual.go" > "${SCRIPT_DIR}/generated.go"
8+
popd > /dev/null

script/constructor.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package script
2+
3+
// TODO: generate constructrs from bitfield/script's source code
4+
5+
import (
6+
"context"
7+
"net/http"
8+
9+
"github.com/bitfield/script"
10+
)
11+
12+
func newPipeFrom(pipe *script.Pipe) *Pipe {
13+
return &Pipe{Pipe: pipe}
14+
}
15+
16+
func NewPipe() *Pipe {
17+
return newPipeFrom(script.NewPipe())
18+
}
19+
20+
func At(dir string) *Pipe {
21+
return NewPipe().At(dir)
22+
}
23+
24+
func Args() *Pipe {
25+
return newPipeFrom(script.Args())
26+
}
27+
28+
func Do(req *http.Request) *Pipe {
29+
return newPipeFrom(script.Do(req))
30+
}
31+
32+
func File(path string) *Pipe {
33+
return newPipeFrom(script.File(path))
34+
}
35+
36+
func Get(url string) *Pipe {
37+
return newPipeFrom(script.Get(url))
38+
}
39+
40+
func Post(url string) *Pipe {
41+
return newPipeFrom(script.Post(url))
42+
}
43+
44+
func Echo(s string) *Pipe {
45+
return newPipeFrom(script.Echo(s))
46+
}
47+
48+
func Slice(s []string) *Pipe {
49+
return newPipeFrom(script.Slice(s))
50+
}
51+
52+
func Stdin() *Pipe {
53+
return newPipeFrom(script.Stdin())
54+
}
55+
56+
func Exec(cmdLine string) *Pipe {
57+
return newPipeFrom(script.Exec(cmdLine))
58+
}
59+
60+
func ExecContext(ctx context.Context, cmdLine string) *Pipe {
61+
return NewPipe().ExecContext(ctx, cmdLine)
62+
}
63+
64+
func ListFiles(path string) *Pipe {
65+
return newPipeFrom(script.ListFiles(path))
66+
}
67+
68+
func FindFiles(path string) *Pipe {
69+
return newPipeFrom(script.FindFiles(path))
70+
}
71+
72+
func IfExists(path string) *Pipe {
73+
return newPipeFrom(script.IfExists(path))
74+
}

0 commit comments

Comments
 (0)