package ini import ( "bufio" "io" "os" "sort" "strings" "gitea.zaclys.com/bvaudour/gob/convert" . "gitea.zaclys.com/bvaudour/gob/option" ) /* Setting représente le contenu d’un fichier de configuration de la forme .ini. Un fichier .ini a la forme suivante : [section1] clé1 = valeur clé2 = valeur ; un commentaire éventuel #un autre commentaire [section2] clé1 = valeur <...> L’accès aux clés de la configuration se fait en préfixant la clé souhaitée par la section d’appartenance suivie d’un point (exemple : section1.clé1). Les types de valeurs supportées actuellement sont : - les chaînes de caractère - les booléens : 0, 1, true, false - les entiers - les tableaux de chaînes : ce sont des chaînes séparées par des virgules La conversion entre ces différents types se fait sans panic et aucun contrôle n’est effectué sur la cohérence des données. */ type Setting struct { t []string m map[string]string i map[string]int } // New crée une nouvelle configuration à partir d’un template. // Le template doit avoir la structure d’un fichier .ini. func New(template string) *Setting { c := &Setting{ m: make(map[string]string), i: make(map[string]int), } b := bufio.NewScanner(strings.NewReader(template)) var section string i := -1 for b.Scan() { i++ line := b.Text() c.t = append(c.t, line) line = strings.TrimSpace(line) l := len(line) // Line is comment or blank line if l == 0 || line[0] == '#' || line[0] == ';' { continue } // line is section header if line[0] == '[' && line[l-1] == ']' { section = line[1 : l-1] continue } if idx := strings.Index(line, "="); idx > 0 { key, value := strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:]) k := section + "." + key c.m[k] = value c.i[k] = i } } return c } // Keys retourne la liste des clés de la configuration. // Chaque clé est des la forme section.clé. func (c *Setting) Keys() []string { out := make([]string, 0, len(c.i)) for i := range c.i { out = append(out, i) } sort.Slice(out, func(i, j int) bool { return c.i[out[i]] < c.i[out[j]] }) return out } // Position retourne le numéro de ligne dans laquelle se trouve la clé donnée. // Les n° de ligne commencent à partir de 0. func (c *Setting) Position(key string) (position Option[int]) { if p, ok := c.i[key]; ok { position = Some(p) } return } func conv(e any) string { var s string t := convert.TypeOf(e) switch { case t.Is(convert.Bool): s = "0" if b := convert.ToBool(e, false); b { s = "1" } case t.Is(convert.Slice): a := convert.ToSlice[string](e) s = strings.Join(a, ",") default: s = convert.ToString(e, "") } return s } // Set définit la clé key à la valeur value. // Si la clé n’existe pas, aucune modification n’est effectuée et la méthode retourne false. func (c *Setting) Set(key string, value any) (ok bool) { if _, ok = c.m[key]; ok { c.m[key] = conv(value) } return } // Get retourne la valeur de la clé demandée. // Si celle-ci n’existe pas, une chaîne vide est retournée. func (c *Setting) Get(key string) string { return c.m[key] } // GetBool retourne la valeur de la clé demandée, convertie en booléen. func (c *Setting) GetBool(key string) (out bool) { out = convert.ToBool(c.Get(key), false) return } // GetInt retourne la valeur de la clé demandée, convertie en entier. func (c *Setting) GetInt(key string) (out int) { out = convert.ToInt(c.Get(key), 0) return } // GetUint retourne la valeur de la clé demandée, convertie en entier non signé. func (c *Setting) GetUint(key string) (out uint) { out = convert.ToUint(c.Get(key), uint(0)) return } // GetSlice retourne la valeur de la clé demandée, convertie en slice. func (c *Setting) GetSlice(key string) (out []string) { v := c.Get(key) if len(v) > 0 { out = strings.Split(v, ",") } return } // Read parse la configuration du descripteur d’entrée. func (c *Setting) Read(r io.Reader) { b := bufio.NewScanner(r) var section string for b.Scan() { line := b.Text() line = strings.TrimSpace(line) l := len(line) // Line is comment or blank line if l == 0 || line[0] == '#' || line[0] == ';' { continue } // line is section header if line[0] == '[' && line[l-1] == ']' { section = line[1 : l-1] continue } if i := strings.Index(line, "="); i > 0 && i < l-1 { key, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) k := section + "." + key c.m[k] = value } } } // Write écrit la configuration dans le descripteur de sortie. func (c *Setting) Write(w io.Writer) (out Option[error]) { for k, v := range c.m { l := c.t[c.i[k]] i := strings.Index(l, "=") c.t[c.i[k]] = l[:i+1] + " " + v } buf := bufio.NewWriter(w) for _, l := range c.t { if _, err := buf.WriteString(l + "\n"); err != nil { return Some(err) } } if err := buf.Flush(); err != nil { out = Some(err) } return } // IniFile représente un fichier de configuration. type IniFile struct { *Setting Path string } // NewFile génère un nouveau fichier de de configuration. // Rien n’est inscrit sur le chemin donné et cela reste en mémoire // tant que la méthode Save n’est pas appelée explicitement. // Par ailleurs, le fichier dans le filepath n’est pas parsé // tant que la méthode Load n’est pas appelée explicitement. func NewFile(template, filepath string) *IniFile { return &IniFile{ Setting: New(template), Path: filepath, } } // Load charge la configuration func (cf *IniFile) Load() (out Option[error]) { if f, err := os.Open(cf.Path); err == nil { defer f.Close() cf.Setting.Read(f) } else { out = Some(err) } return } // Save enregistre la configuration sur le disque. func (cf *IniFile) Save() (out Option[error]) { if f, err := os.Create(cf.Path); err == nil { defer f.Close() out = cf.Setting.Write(f) } else { out = Some(err) } return }