123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package config
- import (
- "bufio"
- "bytes"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "kitty/tools/utils"
- )
- var _ = fmt.Print
- func StringToBool(x string) bool {
- x = strings.ToLower(x)
- return x == "y" || x == "yes" || x == "true"
- }
- type ConfigLine struct {
- Src_file, Line string
- Line_number int
- Err error
- }
- type ConfigParser struct {
- LineHandler func(key, val string) error
- CommentsHandler func(line string) error
- SourceHandler func(text, path string)
- bad_lines []ConfigLine
- seen_includes map[string]bool
- override_env []string
- }
- type Scanner interface {
- Scan() bool
- Text() string
- Err() error
- }
- func (self *ConfigParser) BadLines() []ConfigLine {
- return self.bad_lines
- }
- var key_pat = sync.OnceValue(func() *regexp.Regexp {
- return regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$`)
- })
- func (self *ConfigParser) parse(scanner Scanner, name, base_path_for_includes string, depth int) error {
- if self.seen_includes[name] { // avoid include loops
- return nil
- }
- self.seen_includes[name] = true
- recurse := func(r io.Reader, nname, base_path_for_includes string) error {
- if depth > 32 {
- return fmt.Errorf("Too many nested include directives while processing config file: %s", name)
- }
- escanner := bufio.NewScanner(r)
- return self.parse(escanner, nname, base_path_for_includes, depth+1)
- }
- make_absolute := func(path string) (string, error) {
- if path == "" {
- return "", fmt.Errorf("Empty include paths not allowed")
- }
- if !filepath.IsAbs(path) {
- path = filepath.Join(base_path_for_includes, path)
- }
- return path, nil
- }
- lnum := 0
- next_line_num := 0
- next_line := ""
- var line string
- for {
- if next_line != "" {
- line = next_line
- } else {
- if scanner.Scan() {
- line = strings.TrimLeft(scanner.Text(), " \t")
- next_line_num++
- } else {
- break
- }
- if line == "" {
- continue
- }
- }
- lnum = next_line_num
- if scanner.Scan() {
- next_line = strings.TrimLeft(scanner.Text(), " \t")
- next_line_num++
- for strings.HasPrefix(next_line, `\`) {
- line += next_line[1:]
- if scanner.Scan() {
- next_line = strings.TrimLeft(scanner.Text(), " \t")
- next_line_num++
- } else {
- next_line = ""
- }
- }
- } else {
- next_line = ""
- }
- if line[0] == '#' {
- if self.CommentsHandler != nil {
- err := self.CommentsHandler(line)
- if err != nil {
- self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: err})
- }
- }
- continue
- }
- m := key_pat().FindStringSubmatch(line)
- if len(m) < 3 {
- self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: fmt.Errorf("Invalid config line: %#v", line)})
- continue
- }
- key, val := m[1], m[2]
- for i, ch := range line {
- if ch == ' ' || ch == '\t' {
- key = line[:i]
- val = strings.TrimSpace(line[i+1:])
- break
- }
- }
- switch key {
- default:
- err := self.LineHandler(key, val)
- if err != nil {
- self.bad_lines = append(self.bad_lines, ConfigLine{Src_file: name, Line: line, Line_number: lnum, Err: err})
- }
- case "include", "globinclude", "envinclude":
- var includes []string
- switch key {
- case "include":
- aval, err := make_absolute(val)
- if err == nil {
- includes = []string{aval}
- }
- case "globinclude":
- aval, err := make_absolute(val)
- if err == nil {
- matches, err := filepath.Glob(aval)
- if err == nil {
- includes = matches
- }
- }
- case "envinclude":
- env := self.override_env
- if env == nil {
- env = os.Environ()
- }
- for _, x := range env {
- key, eval, _ := strings.Cut(x, "=")
- is_match, err := filepath.Match(val, key)
- if is_match && err == nil {
- err := recurse(strings.NewReader(eval), "<env var: "+key+">", base_path_for_includes)
- if err != nil {
- return err
- }
- }
- }
- }
- if len(includes) > 0 {
- for _, incpath := range includes {
- raw, err := os.ReadFile(incpath)
- if err == nil {
- err := recurse(bytes.NewReader(raw), incpath, filepath.Dir(incpath))
- if err != nil {
- return err
- }
- } else if !errors.Is(err, fs.ErrNotExist) {
- return fmt.Errorf("Failed to process include %#v with error: %w", incpath, err)
- }
- }
- }
- }
- }
- return nil
- }
- func (self *ConfigParser) ParseFiles(paths ...string) error {
- for _, path := range paths {
- apath, err := filepath.Abs(path)
- if err == nil {
- path = apath
- }
- raw, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- scanner := utils.NewLineScanner(utils.UnsafeBytesToString(raw))
- self.seen_includes = make(map[string]bool)
- err = self.parse(scanner, path, filepath.Dir(path), 0)
- if err != nil {
- return err
- }
- if self.SourceHandler != nil {
- self.SourceHandler(utils.UnsafeBytesToString(raw), path)
- }
- }
- return nil
- }
- func (self *ConfigParser) LoadConfig(name string, paths []string, overrides []string) (err error) {
- const SYSTEM_CONF = "/etc/xdg/kitty"
- system_conf := filepath.Join(SYSTEM_CONF, name)
- add_if_exists := func(q string) {
- err = self.ParseFiles(q)
- if err != nil && errors.Is(err, fs.ErrNotExist) {
- err = nil
- }
- }
- if add_if_exists(system_conf); err != nil {
- return err
- }
- if len(paths) > 0 {
- for _, path := range paths {
- if add_if_exists(path); err != nil {
- return err
- }
- }
- } else {
- if add_if_exists(filepath.Join(utils.ConfigDirForName(name), name)); err != nil {
- return err
- }
- }
- if len(overrides) > 0 {
- err = self.ParseOverrides(overrides...)
- if err != nil {
- return err
- }
- }
- return
- }
- type LinesScanner struct {
- lines []string
- }
- func (self *LinesScanner) Scan() bool {
- return len(self.lines) > 0
- }
- func (self *LinesScanner) Text() string {
- ans := self.lines[0]
- self.lines = self.lines[1:]
- return ans
- }
- func (self *LinesScanner) Err() error {
- return nil
- }
- func (self *ConfigParser) ParseOverrides(overrides ...string) error {
- s := LinesScanner{lines: utils.Map(func(x string) string {
- return strings.Replace(x, "=", " ", 1)
- }, overrides)}
- self.seen_includes = make(map[string]bool)
- return self.parse(&s, "<overrides>", utils.ConfigDir(), 0)
- }
|