package gojq import ( "encoding/json" "fmt" "io" "os" "path/filepath" "strings" ) // ModuleLoader is the interface for loading modules. // // Implement following optional methods. Use [NewModuleLoader] to load local modules. // // LoadInitModules() ([]*Query, error) // LoadModule(string) (*Query, error) // LoadModuleWithMeta(string, map[string]any) (*Query, error) // LoadJSON(string) (any, error) // LoadJSONWithMeta(string, map[string]any) (any, error) type ModuleLoader any // NewModuleLoader creates a new [ModuleLoader] loading local modules in the paths. // Note that user can load modules outside the paths using "search" path of metadata. // Empty paths are ignored, so specify "." for the current working directory. func NewModuleLoader(paths []string) ModuleLoader { ps := make([]string, 0, len(paths)) for _, path := range paths { if path = resolvePath(path, ""); path != "" { ps = append(ps, path) } } return &moduleLoader{ps} } type moduleLoader struct { paths []string } func (l *moduleLoader) LoadInitModules() ([]*Query, error) { var qs []*Query for _, path := range l.paths { if filepath.Base(path) != ".jq" { continue } fi, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { continue } return nil, err } if fi.IsDir() { continue } cnt, err := os.ReadFile(path) if err != nil { return nil, err } q, err := parseModule(string(cnt), filepath.Dir(path)) if err != nil { return nil, &queryParseError{path, string(cnt), err} } qs = append(qs, q) } return qs, nil } func (l *moduleLoader) LoadModuleWithMeta(name string, meta map[string]any) (*Query, error) { path, err := l.lookupModule(name, ".jq", meta) if err != nil { return nil, err } cnt, err := os.ReadFile(path) if err != nil { return nil, err } q, err := parseModule(string(cnt), filepath.Dir(path)) if err != nil { return nil, &queryParseError{path, string(cnt), err} } return q, nil } func (l *moduleLoader) LoadJSONWithMeta(name string, meta map[string]any) (any, error) { path, err := l.lookupModule(name, ".json", meta) if err != nil { return nil, err } f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() vals := []any{} dec := json.NewDecoder(f) dec.UseNumber() for { var val any if err := dec.Decode(&val); err != nil { if err == io.EOF { break } if _, err := f.Seek(0, io.SeekStart); err != nil { return nil, err } cnt, er := io.ReadAll(f) if er != nil { return nil, er } return nil, &jsonParseError{path, string(cnt), err} } vals = append(vals, val) } return vals, nil } func (l *moduleLoader) lookupModule(name, extension string, meta map[string]any) (string, error) { paths := l.paths if path, ok := meta["search"].(string); ok { if path = resolvePath(path, ""); path != "" { paths = append([]string{path}, paths...) } } for _, base := range paths { path := filepath.Join(base, name+extension) if _, err := os.Stat(path); err == nil { return path, err } path = filepath.Join(base, name, filepath.Base(name)+extension) if _, err := os.Stat(path); err == nil { return path, err } } return "", fmt.Errorf("module not found: %q", name) } func parseModule(cnt, dir string) (*Query, error) { q, err := Parse(cnt) if err != nil { return nil, err } for _, i := range q.Imports { if i.Meta != nil { for _, e := range i.Meta.KeyVals { if e.Key == "search" || e.KeyString == "search" { if path, ok := e.Val.toString(); ok { if path = resolvePath(path, dir); path != "" { e.Val.Str = path } else { e.Val.Null = true } } } } } } return q, nil } func resolvePath(path, dir string) string { switch { case filepath.IsAbs(path): return path case strings.HasPrefix(path, "~/"): dir, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(dir, path[2:]) case strings.HasPrefix(path, "$ORIGIN/"): exe, err := os.Executable() if err != nil { return "" } exe, err = filepath.EvalSymlinks(exe) if err != nil { return "" } return filepath.Join(filepath.Dir(exe), path[8:]) default: return filepath.Join(dir, path) } }