This commit is contained in:
GenZmeY 2020-04-26 02:42:14 +03:00
parent 753cb7bfd6
commit 294a89ba74
59 changed files with 1840 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# Binaries for programs and plugins
bin/*
multini
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Custom binary test
tests/out_ini/*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

73
Makefile Normal file
View File

@ -0,0 +1,73 @@
NAME=multini
VERSION=0.1
GOCMD=go
LDFLAGS:="$(LDFLAGS) -X 'main.Version=$(VERSION)'"
GOBUILD=$(GOCMD) build -ldflags=$(LDFLAGS)
SRCMAIN=.
BINDIR=bin
BIN=$(BINDIR)/$(NAME)
README=README
LICENSE=LICENSE
TEST=./run_test.sh
PREFIX=/usr
all: build
prep: clean
mkdir $(BINDIR)
build: prep
$(GOBUILD) -o $(BIN) $(SRCMAIN)
check-build:
test -e $(BIN)
freebsd-386: prep
GOOS=freebsd GOARCH=386 $(GOBUILD) -o $(BIN)-freebsd-386 $(SRCMAIN)
darwin-386: prep
GOOS=darwin GOARCH=386 $(GOBUILD) -o $(BIN)-darwin-386 $(SRCMAIN)
linux-386: prep
GOOS=linux GOARCH=386 $(GOBUILD) -o $(BIN)-linux-386 $(SRCMAIN)
windows-386: prep
GOOS=windows GOARCH=386 $(GOBUILD) -o $(BIN)-windows-386.exe $(SRCMAIN)
freebsd-amd64: prep
GOOS=freebsd GOARCH=amd64 $(GOBUILD) -o $(BIN)-freebsd-amd64 $(SRCMAIN)
darwin-amd64: prep
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BIN)-darwin-amd64 $(SRCMAIN)
linux-amd64: prep
GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BIN)-linux-amd64 $(SRCMAIN)
windows-amd64: prep
GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BIN)-windows-amd64.exe $(SRCMAIN)
compile: freebsd-386 darwin-386 linux-386 windows-386 freebsd-amd64 darwin-amd64 linux-amd64 windows-amd64
install: check-build
install -m 755 -d $(PREFIX)/bin/
install -m 755 $(BIN) $(PREFIX)/bin/
install -m 755 -d $(PREFIX)/share/licenses/$(NAME)/
install -m 644 $(LICENSE) $(PREFIX)/share/licenses/$(NAME)/
install -m 755 -d $(PREFIX)/share/doc/$(NAME)/
install -m 644 $(README) $(PREFIX)/share/doc/$(NAME)/
check-install:
test -e $(PREFIX)/bin/$(NAME) || \
test -d $(PREFIX)/share/licenses/$(NAME) || \
test -d $(PREFIX)/share/doc/$(NAME)
uninstall: check-install
rm -f $(PREFIX)/bin/$(NAME)
rm -rf $(PREFIX)/share/licenses/$(NAME)
rm -rf $(PREFIX)/share/doc/$(NAME)
clean:
rm -rf $(BINDIR)
test: check-build
$(TEST) $(BIN)

22
README Normal file
View File

@ -0,0 +1,22 @@
A utility for manipulating ini files with duplicate keys
Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]
Actions:
-g, --get get values for a given combination of parameters.
-s, --set set values for a given combination of parameters.
-a, --add add values for a given combination of parameters.
-d, --del delete the given combination of parameters.
-c, --chk display parsing errors for the specified file.
Options:
-e, --existing For --set and --del, fail if item is missing.
-i, --inplace Lock and write files in place.
This is not atomic but has less restrictions
than the default replacement method.
-o, --output FILE Write output to FILE instead. '-' means stdout
-u, --unix Use LF as end of line
-w, --windows Use CRLF as end of line
-h, --help Write this help to stdout
--version Write version to stdout
2020 (c) GenZmeY

68
actions.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"multini/output"
"multini/types"
)
func chk() int {
var ok bool
var err error
ok, err = iniCheck(ArgFile)
if err != nil {
output.Errorln(err)
return EXIT_FILE_READ_ERR
}
if ok {
return EXIT_SUCCESS
} else {
return EXIT_BAD_SYNTAX_ERR
}
}
func add(ini *types.Ini) error {
if ArgKeyIsSet {
return ini.AddKey(ArgSection, ArgKey, ArgValue)
} else {
ini.AddSection(ArgSection)
return nil
}
}
func set(ini *types.Ini) error {
if ArgKeyIsSet {
return ini.SetKey(ArgSection, ArgKey, ArgValue)
} else {
ini.SetSection(ArgSection)
return nil
}
}
func get(ini *types.Ini) error {
var err error = nil
var res string
if ArgValueIsSet {
return ini.GetKeyVal(ArgSection, ArgKey, ArgValue)
} else if ArgKeyIsSet {
res, err = ini.GetKey(ArgSection, ArgKey)
} else if ArgSectionIsSet {
res, err = ini.GetSection(ArgSection)
} else {
res = ini.Get()
}
if err == nil {
output.Println(res)
}
return err
}
func del(ini *types.Ini) error {
if ArgValueIsSet {
return ini.DelKeyVal(ArgSection, ArgKey, ArgValue)
} else if ArgKeyIsSet {
return ini.DelKey(ArgSection, ArgKey)
} else {
return ini.DelSection(ArgSection)
}
}

177
args.go Normal file
View File

@ -0,0 +1,177 @@
package main
import (
"errors"
"os"
"multini/output"
"multini/types"
"github.com/juju/gnuflag"
)
var (
ArgInplace bool
ArgGet bool
ArgSet bool
ArgAdd bool
ArgDel bool
ArgChk bool
ArgVerbose bool
ArgVersion bool
ArgWindows bool
ArgUnix bool
ArgHelp bool
ArgExisting bool
ArgOutput string
ArgFile string
ArgFileIsSet bool = false
ArgSection string
ArgSectionIsSet bool = false
ArgKey string
ArgKeyIsSet bool = false
ArgValue string
ArgValueIsSet bool = false
)
func printHelp() {
output.Println("A utility for manipulating ini files with duplicate keys")
output.Println("")
output.Println("Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]")
output.Println("Actions:")
output.Println(" -g, --get get values for a given combination of parameters.")
output.Println(" -s, --set set values for a given combination of parameters.")
output.Println(" -a, --add add values for a given combination of parameters.")
output.Println(" -d, --del delete the given combination of parameters.")
output.Println(" -c, --chk display parsing errors for the specified file.")
output.Println("")
output.Println("Options:")
output.Println(" -e, --existing For --set and --del, fail if item is missing.")
output.Println(" -i, --inplace Lock and write files in place.")
output.Println(" This is not atomic but has less restrictions")
output.Println(" than the default replacement method.")
output.Println(" -o, --output FILE Write output to FILE instead. '-' means stdout")
// output.Println(" -v, --verbose Indicate on stderr if changes were made")
output.Println(" -u, --unix Use LF as end of line")
output.Println(" -w, --windows Use CRLF as end of line")
output.Println(" -h, --help Write this help to stdout")
output.Println(" --version Write version to stdout")
}
func printVersion() {
output.Println("multini ", Version)
}
func init() {
gnuflag.BoolVar(&ArgGet, "get", false, "")
gnuflag.BoolVar(&ArgGet, "g", false, "")
gnuflag.BoolVar(&ArgAdd, "add", false, "")
gnuflag.BoolVar(&ArgAdd, "a", false, "")
gnuflag.BoolVar(&ArgSet, "set", false, "")
gnuflag.BoolVar(&ArgSet, "s", false, "")
gnuflag.BoolVar(&ArgDel, "del", false, "")
gnuflag.BoolVar(&ArgDel, "d", false, "")
gnuflag.BoolVar(&ArgChk, "chk", false, "")
gnuflag.BoolVar(&ArgChk, "c", false, "")
gnuflag.BoolVar(&ArgDel, "inplace", false, "")
gnuflag.BoolVar(&ArgDel, "i", false, "")
gnuflag.BoolVar(&ArgUnix, "unix", false, "")
gnuflag.BoolVar(&ArgUnix, "u", false, "")
gnuflag.BoolVar(&ArgWindows, "windows", false, "")
gnuflag.BoolVar(&ArgWindows, "w", false, "")
gnuflag.BoolVar(&ArgExisting, "existing", false, "")
gnuflag.BoolVar(&ArgExisting, "e", false, "")
gnuflag.BoolVar(&ArgVerbose, "verbose", false, "")
gnuflag.BoolVar(&ArgVerbose, "v", false, "")
gnuflag.StringVar(&ArgOutput, "output", "", "")
gnuflag.StringVar(&ArgOutput, "o", "", "")
gnuflag.BoolVar(&ArgVersion, "version", false, "")
gnuflag.BoolVar(&ArgHelp, "help", false, "")
gnuflag.BoolVar(&ArgHelp, "h", false, "")
}
func parseArgs() error {
gnuflag.Parse(true)
// info
switch {
case ArgHelp:
printHelp()
os.Exit(EXIT_SUCCESS)
case ArgVersion:
printVersion()
os.Exit(EXIT_SUCCESS)
}
// File EOF
switch {
case ArgWindows:
types.SetEndOfLineWindows()
case ArgUnix:
types.SetEndOfLineUnix()
default:
types.SetEndOfLineNative()
}
// Output settings
output.SetEndOfLineNative()
output.SetVerbose(ArgVerbose)
// Positional Args
for i := 0; i < 4 && i < gnuflag.NArg(); i++ {
switch i {
case 0:
ArgFile = gnuflag.Arg(0)
ArgFileIsSet = true
case 1:
ArgSection = gnuflag.Arg(1)
ArgSectionIsSet = true
case 2:
ArgKey = gnuflag.Arg(2)
ArgKeyIsSet = true
case 3:
ArgValue = gnuflag.Arg(3)
ArgValueIsSet = true
}
}
if !ArgFileIsSet {
return errors.New("Config_file not specified")
}
// Mode
actionCounter := 0
if ArgChk {
actionCounter++
}
if ArgDel {
actionCounter++
if !ArgSectionIsSet {
return errors.New("Section not specified")
}
}
if ArgGet {
actionCounter++
}
if ArgSet {
actionCounter++
if !ArgSectionIsSet {
return errors.New("Section not specified")
}
}
if ArgAdd {
actionCounter++
if !ArgSectionIsSet {
return errors.New("Section not specified")
}
}
switch actionCounter {
case 0:
return errors.New("Action not set")
case 1:
return nil
default:
return errors.New("Only one action can be used at the same time")
}
}

80
main.go Normal file
View File

@ -0,0 +1,80 @@
package main
import (
"fmt"
"os"
"multini/output"
"multini/types"
)
const (
EXIT_SUCCESS int = 0
EXIT_ARG_ERR int = 1
EXIT_FILE_READ_ERR int = 2
EXIT_BAD_SYNTAX_ERR int = 3
EXIT_ELEMENT_NOT_FOUND int = 4
EXIT_FILE_WRITE_ERR int = 5
EXIT_ACTION_ERR int = 6
)
var (
Version string = "development"
)
func main() {
var err error
var ini types.Ini
if err = parseArgs(); err != nil {
output.Errorln(err)
os.Exit(EXIT_ARG_ERR)
}
if ArgChk {
os.Exit(chk())
}
ini, err = iniRead(ArgFile)
if err != nil {
fmt.Println(err)
os.Exit(EXIT_FILE_READ_ERR)
}
switch {
case ArgGet:
err = get(&ini)
case ArgAdd:
err = add(&ini)
case ArgDel:
err = del(&ini)
case ArgSet:
err = set(&ini)
}
if err != nil {
output.Errorln(err)
os.Exit(EXIT_ACTION_ERR)
}
if ArgOutput == "-" {
output.Println(ini.Full())
os.Exit(EXIT_SUCCESS)
} else if ArgOutput != "" {
ArgFile = ArgOutput
}
if ArgAdd || ArgSet || ArgDel {
if ArgInplace || ArgOutput != "" {
err = iniWriteInplace(ArgFile, &ini)
} else {
err = iniWrite(ArgFile, &ini)
}
if err != nil {
output.Errorln(err)
os.Exit(EXIT_FILE_WRITE_ERR)
}
}
os.Exit(EXIT_SUCCESS)
}

82
output/output.go Normal file
View File

@ -0,0 +1,82 @@
package output
import (
"fmt"
"io/ioutil"
"log"
"os"
"runtime"
)
var (
endOfLine string = "\n"
devNull *log.Logger = log.New(ioutil.Discard, "", 0)
stdout *log.Logger = log.New(os.Stdout, "", 0)
stderr *log.Logger = log.New(os.Stderr, "", 0)
verbose *log.Logger = devNull
)
func SetVerbose(enabled bool) {
if enabled {
verbose = stderr
} else {
verbose = devNull
}
}
func SetEndOfLineNative() {
switch os := runtime.GOOS; os {
case "windows":
setEndOfLineWindows()
default:
setEndOfLineUnix()
}
}
func EOL() string {
return endOfLine
}
func setEndOfLineUnix() {
endOfLine = "\n"
}
func setEndOfLineWindows() {
endOfLine = "\r\n"
}
func Print(v ...interface{}) {
stdout.Print(v...)
}
func Printf(format string, v ...interface{}) {
stdout.Printf(format, v...)
}
func Println(v ...interface{}) {
stdout.Print(fmt.Sprint(v...) + endOfLine)
}
func Error(v ...interface{}) {
stderr.Print(v...)
}
func Errorf(format string, v ...interface{}) {
stderr.Printf(format, v...)
}
func Errorln(v ...interface{}) {
stderr.Print(fmt.Sprint(v...) + endOfLine)
}
func Verbose(v ...interface{}) {
verbose.Print(v...)
}
func Verbosef(format string, v ...interface{}) {
verbose.Printf(format, v...)
}
func Verboseln(v ...interface{}) {
verbose.Print(fmt.Sprint(v...) + endOfLine)
}

182
reader.go Normal file
View File

@ -0,0 +1,182 @@
package main
import (
"bufio"
"errors"
"os"
"regexp"
"strings"
"multini/output"
"multini/types"
)
var (
// Ng - Named Group
NgPrefix string = `prefix`
NgPostifx string = `postfix`
NgSection string = `section`
NgKey string = `key`
NgKeyPostfix string = `key_postfix`
NgValue string = `value`
NgValuePrefix string = `value_prefix`
NgValuePostfix string = `value_postfix`
NgComment string = `comment`
NgCommentPrefix string = `comment_prefix`
RxBodyPrefix string = `(?P<` + NgPrefix + `>\s+)?`
RxSectionName string = `\[(?P<` + NgSection + `>.+)\]`
RxKey string = `(?P<` + NgKey + `>(?:[^;#=]+[^\s=;#]|[^;#=]))?`
RxKeyPostfix string = `(?P<` + NgKeyPostfix + `>\s+)?`
RxValuePrefix string = `(?P<` + NgValuePrefix + `>\s+)?`
RxValue string = `(?P<` + NgValue + `>(?:[^;#]+[^\s;#]|[^;#]))?`
RxValuePostfix string = `(?P<` + NgValuePostfix + `>\s+)?`
RxKeyVal string = RxKey + RxKeyPostfix + `=` + RxValuePrefix + RxValue + RxValuePostfix
RxBody string = `(?:` + RxSectionName + `|` + RxKeyVal + `)?`
RxBodyPostfix string = `(?P<` + NgPostifx + `>\s+)?`
RxCommentPrefix string = `(?P<` + NgCommentPrefix + `>[#;]\s*)`
RxCommentText string = `(?P<` + NgComment + `>.+)?`
RxComment string = `(?:` + RxCommentPrefix + RxCommentText + `)?`
Rx string = RxBodyPrefix + RxBody + RxBodyPostfix + RxComment
RxCompiled *regexp.Regexp = regexp.MustCompile(Rx)
)
func rxParse(rx *regexp.Regexp, str string) map[string]string {
match := rx.FindStringSubmatch(str)
result := make(map[string]string)
for i, name := range rx.SubexpNames() {
if i != 0 && name != "" && i <= len(match) {
result[name] = match[i]
}
}
return result
}
func debugMap(el map[string]string) string {
var dbgMap strings.Builder
for key, val := range el {
dbgMap.WriteString(" " + key + ": \"" + val + "\"" + output.EOL())
}
return dbgMap.String()
}
func appendLine(ini *types.Ini, line string) error {
elements := rxParse(RxCompiled, line)
switch {
case elements[NgSection] != "":
var newSection types.Section
newSection.Name = elements[NgSection]
newSection.Prefix = elements[NgPrefix]
newSection.Postfix = elements[NgPostifx]
newSection.Comment.Prefix = elements[NgCommentPrefix]
newSection.Comment.Value = elements[NgComment]
if newSection.Line() == line {
ini.Sections = append(ini.Sections, &newSection)
return nil
} else {
output.Verboseln("Got:", newSection.Line())
var newTrash types.Trash = types.Trash{Value: line}
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newTrash)
}
case elements[NgKey] != "":
var newKeyValue types.KeyValue
newKeyValue.Key = elements[NgKey]
newKeyValue.PostfixKey = elements[NgKeyPostfix]
newKeyValue.PrefixKey = elements[NgPrefix]
newKeyValue.Value = elements[NgValue]
newKeyValue.PrefixValue = elements[NgValuePrefix]
newKeyValue.PostfixValue = elements[NgValuePostfix]
newKeyValue.Comment.Value = elements[NgComment]
newKeyValue.Comment.Prefix = elements[NgCommentPrefix]
if newKeyValue.Line() == line {
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newKeyValue)
return nil
} else {
output.Verboseln("Got:", newKeyValue.Line())
var newTrash types.Trash = types.Trash{Value: line}
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newTrash)
}
case elements[NgComment] != "":
var newComment types.Comment
newComment.Prefix = elements[NgPrefix] + elements[NgCommentPrefix]
newComment.Value = elements[NgComment]
if newComment.Line() == line {
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newComment)
return nil
} else {
output.Verboseln("Got:", newComment.Line())
var newTrash types.Trash = types.Trash{Value: line}
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newTrash)
}
case elements[NgPrefix] != "" || line == "":
var newEmptyLine types.EmptyLine
newEmptyLine.Prefix = elements[NgPrefix]
if newEmptyLine.Line() == line {
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newEmptyLine)
return nil
} else {
output.Verboseln("Got:", newEmptyLine.Line())
var newTrash types.Trash = types.Trash{Value: line}
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newTrash)
}
default:
var newTrash types.Trash = types.Trash{Value: line}
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newTrash)
}
return errors.New(debugMap(elements))
}
func iniRead(filename string) (types.Ini, error) {
var (
err error
iniFile *os.File
ini types.Ini
)
iniFile, err = os.Open(filename)
if err != nil {
return ini, err
}
fileScanner := bufio.NewScanner(iniFile)
fileScanner.Split(bufio.ScanLines)
ini.Init()
for i := 1; fileScanner.Scan(); i++ {
appendLine(&ini, fileScanner.Text())
}
iniFile.Close()
return ini, nil
}
func iniCheck(filename string) (bool, error) {
var (
err error
iniFile *os.File
ini types.Ini
ok bool = true
)
iniFile, err = os.Open(filename)
if err != nil {
return ok, err
}
fileScanner := bufio.NewScanner(iniFile)
fileScanner.Split(bufio.ScanLines)
ini.Init()
for i := 1; fileScanner.Scan(); i++ {
line := fileScanner.Text()
err = appendLine(&ini, line)
if err != nil {
output.Errorln(i, ":", line)
output.Verboseln(err)
ok = false
}
}
iniFile.Close()
return ok, nil
}

29
run_test.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
DEF='\e[0m'; BLD='\e[1m'; RED='\e[31m'; GRN='\e[32m'; WHT='\e[97m'
ScriptFullname=$(readlink -e "$0")
ScriptName=$(echo "$ScriptFullname" | awk -F '/' '{print $NF;}')
ScriptDir=$(dirname "$ScriptFullname")
TestDir="$ScriptDir/tests"
Multini=$(readlink -e "$1")
if [[ -z "$Multini" ]]; then
Multini="$ScriptDir/multini"
fi
RetCode=0
pushd "$TestDir" > /dev/null
while read Test
do
echo -ne "${BLD}${WHT}[----]${DEF} $Test"
Errors=$("$TestDir/$Test" "$Multini")
if [[ $? -ne 0 ]]; then
echo -e "\r${BLD}${WHT}[${RED}FAIL${WHT}]${DEF} $Test"
RetCode=1
echo "$Errors"
else
echo -e "\r${BLD}${WHT}[${GRN} OK ${WHT}]${DEF} $Test"
fi
done < <(find "$TestDir" -type f -name 'test_*.sh' -printf "%f\n")
popd > /dev/null
exit "$RetCode"

27
tests/common.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
ScriptFullname=$(readlink -e "$0")
ScriptName=$(echo "$ScriptFullname" | awk -F '/' '{print $NF;}')
ScriptDir=$(dirname "$ScriptFullname")
Multini="$1"
InDir="$ScriptDir/in_ini"
InIni="$InDir/$ScriptName.ini"
OutDir="$ScriptDir/out_ini"
OutIni="$OutDir/$ScriptName.ini"
ExpectedIni="$ScriptDir/expected_ini/$ScriptName.ini"
if [[ ! -d "$OutDir" ]]; then
mkdir -p "$OutDir"
fi
rm -f "$OutIni"
function compare ()
{
if ! cmp -s "$OutIni" "$ExpectedIni" ; then
diff "$OutIni" "$ExpectedIni"
return 1
fi
rm -f "$OutIni"
return 0
}

View File

@ -0,0 +1,20 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
Key3 = 3
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,20 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
Key = 4
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,20 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]
[NewSection]

View File

@ -0,0 +1,18 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,16 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,18 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,17 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1 @@
Some Value2 And Tabs!

View File

@ -0,0 +1,3 @@
1
2
3

View File

@ -0,0 +1,7 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!

View File

@ -0,0 +1,5 @@
SimpleSection
MultipleKeySection
SectionWithIndent
SectionWithoutNewLineBefore

View File

@ -0,0 +1,17 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = onlyone
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[SimpleSection] # Comment For Section
Key1 = 1
Key2 = 2
[MultipleKeySection]
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

7
tests/test_add_keyval.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --add "$InIni" SimpleSection Key3 3
compare

View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --add "$InIni" MultipleKeySection Key 4
compare

7
tests/test_add_section.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --add "$InIni" NewSection
compare

7
tests/test_del_key.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --del "$InIni" SimpleSection Key1
compare

7
tests/test_del_key_multiple.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --del "$InIni" MultipleKeySection Key
compare

7
tests/test_del_keyval.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --del "$InIni" MultipleKeySection Key 2
compare

7
tests/test_del_section.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --del "$InIni" SectionWithIndent
compare

7
tests/test_get_key.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" '' DefKey2 > "$OutIni"
compare

7
tests/test_get_key_multiple.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" MultipleKeySection Key > "$OutIni"
compare

7
tests/test_get_keyval.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" MultipleKeySection Key 3 > "$OutIni"
compare

7
tests/test_get_section.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" '' > "$OutIni"
compare

7
tests/test_get_section_list.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" > "$OutIni"
compare

7
tests/test_set_keyval.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini -o "$OutIni" --set "$InIni" MultipleKeySection Key onlyone
compare

26
types/comment.go Normal file
View File

@ -0,0 +1,26 @@
package types
type Comment struct {
Prefix string
Value string
}
func (obj *Comment) Full() string {
return obj.Prefix + obj.Value
}
func (obj *Comment) Line() string {
return obj.Prefix + obj.Value
}
func (obj *Comment) Fulln() string {
return obj.Full() + endOfLine
}
func (obj *Comment) Type() TElement {
return TComment
}
func (obj *Comment) Indent() string {
return obj.Prefix
}

50
types/common.go Normal file
View File

@ -0,0 +1,50 @@
package types
import (
"runtime"
)
type TElement int
const (
TDeleted TElement = 0
TComment TElement = 1
TEmptyLine TElement = 2
TKeyValue TElement = 3
TSection TElement = 4
TTrash TElement = 5
)
var (
endOfLine string = "\n"
existing bool = false
)
func SetEndOfLineNative() {
switch os := runtime.GOOS; os {
case "windows":
SetEndOfLineWindows()
default:
SetEndOfLineUnix()
}
}
func SetEndOfLineUnix() {
endOfLine = "\n"
}
func SetEndOfLineWindows() {
endOfLine = "\r\n"
}
func SetExistingMode(value bool) {
existing = value
}
func createIfNotExist() bool {
return !existing
}
func failIfNotExist() bool {
return existing
}

24
types/deleted.go Normal file
View File

@ -0,0 +1,24 @@
package types
type Deleted struct {
}
func (obj *Deleted) Full() string {
return ""
}
func (obj *Deleted) Line() string {
return ""
}
func (obj *Deleted) Fulln() string {
return ""
}
func (obj *Deleted) Type() TElement {
return TDeleted
}
func (obj *Deleted) Indent() string {
return ""
}

9
types/element.go Normal file
View File

@ -0,0 +1,9 @@
package types
type Element interface {
Full() string
Fulln() string
Line() string
Indent() string
Type() TElement
}

25
types/emptyline.go Normal file
View File

@ -0,0 +1,25 @@
package types
type EmptyLine struct {
Prefix string
}
func (obj *EmptyLine) Full() string {
return obj.Prefix
}
func (obj *EmptyLine) Line() string {
return obj.Prefix
}
func (obj *EmptyLine) Fulln() string {
return obj.Full() + endOfLine
}
func (obj *EmptyLine) Type() TElement {
return TEmptyLine
}
func (obj *EmptyLine) Indent() string {
return obj.Prefix
}

152
types/ini.go Normal file
View File

@ -0,0 +1,152 @@
package types
import (
"errors"
"strings"
)
type Ini struct {
Sections []Element
}
func (obj *Ini) Init() {
obj.Sections = append(obj.Sections, &Section{}) // global section
}
func (obj *Ini) Full() string {
var body strings.Builder
for _, line := range obj.Sections {
body.WriteString(line.Fulln())
}
return body.String()
}
func (obj *Ini) FindSection(name string) (*Section, error) {
for i, sect := range obj.Sections {
if sect.(*Section).Name == name {
return obj.Sections[i].(*Section), nil
}
}
return nil, errors.New("Section not found: " + name)
}
func (obj *Ini) DelSection(name string) error {
gotIt := false
for i, sect := range obj.Sections {
if sect.Type() == TSection &&
sect.(*Section).Name == name {
obj.Sections[i] = &Deleted{}
gotIt = true
}
}
if gotIt {
return nil
} else if failIfNotExist() {
return errors.New("Section not found: " + name)
} else {
return nil
}
}
func (obj *Ini) Get() string {
var sectList strings.Builder
for _, sect := range obj.Sections {
name := sect.(*Section).Name
if name != "" || len(sect.(*Section).Lines) > 0 {
sectList.WriteString(name + endOfLine)
}
}
return strings.TrimSuffix(sectList.String(), endOfLine)
}
func (obj *Ini) GetSection(section string) (string, error) {
sect, err := obj.FindSection(section)
if err != nil {
return "", err
} else {
return sect.Full(), nil
}
}
func (obj *Ini) GetKey(section, key string) (string, error) {
sect, err := obj.FindSection(section)
if err != nil {
return "", err
} else {
return sect.GetKey(key)
}
}
func (obj *Ini) GetKeyVal(section, key, value string) error {
sect, err := obj.FindSection(section)
if err != nil {
return err
} else {
return sect.GetKeyVal(key, value)
}
}
func (obj *Ini) AddSection(section string) *Section {
sect, err := obj.FindSection(section)
if err != nil {
var newSection Section
newSection.Name = section
newSection.Prefix = obj.Sections[len(obj.Sections)-1].Indent()
sect = &newSection
obj.Sections = append(obj.Sections, sect)
}
return sect
}
func (obj *Ini) SetSection(section string) *Section {
return obj.AddSection(section)
}
func (obj *Ini) AddKey(section, key, value string) error {
sect, err := obj.FindSection(section)
if err != nil {
if createIfNotExist() {
sect = obj.AddSection(section)
} else {
return err
}
}
sect.AddKey(key, value)
return nil
}
func (obj *Ini) SetKey(section, key, value string) error {
sect, err := obj.FindSection(section)
if err != nil {
if createIfNotExist() {
sect = obj.AddSection(section)
} else {
return err
}
}
return sect.SetKey(key, value)
}
func (obj *Ini) DelKey(section, key string) error {
sect, err := obj.FindSection(section)
if err != nil {
if failIfNotExist() {
return err
} else {
return nil
}
}
return sect.DelKey(key)
}
func (obj *Ini) DelKeyVal(section, key, value string) error {
sect, err := obj.FindSection(section)
if err != nil {
if failIfNotExist() {
return err
} else {
return nil
}
}
return sect.DelKeyVal(key, value)
}

38
types/keyvalue.go Normal file
View File

@ -0,0 +1,38 @@
package types
type KeyValue struct {
PrefixKey string
Key string
PostfixKey string
PrefixValue string
Value string
PostfixValue string
Comment Comment
}
func (obj *KeyValue) Full() string {
return obj.PrefixKey +
obj.Key +
obj.PostfixKey +
"=" +
obj.PrefixValue +
obj.Value +
obj.PostfixValue +
obj.Comment.Full()
}
func (obj *KeyValue) Line() string {
return obj.Full()
}
func (obj *KeyValue) Fulln() string {
return obj.Full() + endOfLine
}
func (obj *KeyValue) Type() TElement {
return TKeyValue
}
func (obj *KeyValue) Indent() string {
return obj.PrefixKey
}

185
types/section.go Normal file
View File

@ -0,0 +1,185 @@
package types
import (
"errors"
"strings"
)
type Section struct {
Prefix string
Name string
Postfix string
Comment Comment
Lines []Element
}
func (obj *Section) Headern() string {
if obj.Name == "" {
return ""
} else {
return obj.Prefix + "[" + obj.Name + "]" + obj.Postfix + obj.Comment.Full() + endOfLine
}
}
func (obj *Section) Line() string {
return obj.Header()
}
func (obj *Section) Header() string {
return strings.TrimSuffix(obj.Headern(), endOfLine)
}
func (obj *Section) Fulln() string {
var body strings.Builder
for _, line := range obj.Lines {
body.WriteString(line.Fulln())
}
return obj.Headern() + body.String()
}
func (obj *Section) Full() string {
return strings.TrimSuffix(obj.Fulln(), endOfLine)
}
func (obj *Section) Type() TElement {
return TSection
}
func (obj *Section) Indent() string {
return obj.Prefix
}
func (obj *Section) DelKey(name string) error {
gotIt := false
for i, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue &&
keyVal.(*KeyValue).Key == name {
obj.Lines[i] = &Deleted{}
gotIt = true
}
}
if gotIt {
return nil
} else if failIfNotExist() {
return errors.New("Parameter not found: " + name)
} else {
return nil
}
}
func (obj *Section) DelKeyVal(name, value string) error {
gotIt := false
for i, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue &&
keyVal.(*KeyValue).Key == name &&
keyVal.(*KeyValue).Value == value {
obj.Lines[i] = &Deleted{}
gotIt = true
}
}
if gotIt {
return nil
} else if failIfNotExist() {
return errors.New("Parameter:value pair not found: " + name + ":" + value)
} else {
return nil
}
}
func (obj *Section) GetKey(name string) (string, error) {
var err error = nil
var result strings.Builder
for _, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue && keyVal.(*KeyValue).Key == name {
result.WriteString(keyVal.(*KeyValue).Value + endOfLine)
}
}
if result.String() == "" {
err = errors.New("Parameter not found: " + name)
}
return strings.TrimSuffix(result.String(), endOfLine), err
}
func (obj *Section) GetKeyVal(name, value string) error {
for _, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue &&
keyVal.(*KeyValue).Key == name &&
keyVal.(*KeyValue).Value == value {
return nil
}
}
return errors.New("Parameter:Value not found: " + name + ":" + value)
}
func (obj *Section) appendKey(name, value string) {
var newKeyValue KeyValue
var replaceIndex int = -1
newKeyValue.Key = name
newKeyValue.Value = value
// replace first emptyline
for i := len(obj.Lines) - 1; i >= 0; i-- {
if obj.Lines[i].Type() == TEmptyLine {
replaceIndex = i
} else {
break
}
}
// for right indent and tabs
for i := len(obj.Lines) - 1; i >= 0; i-- {
if obj.Lines[i].Type() == TKeyValue {
template := obj.Lines[i].(*KeyValue)
newKeyValue.PrefixKey = template.PrefixKey
newKeyValue.PostfixKey = template.PostfixKey
newKeyValue.PrefixValue = template.PrefixValue
newKeyValue.PostfixValue = template.PostfixValue
break
}
}
if replaceIndex == -1 {
obj.Lines = append(obj.Lines, &newKeyValue)
} else {
obj.Lines = append(obj.Lines, obj.Lines[replaceIndex])
obj.Lines[replaceIndex] = &newKeyValue
}
}
func (obj *Section) AddKey(name, value string) {
gotIt := false
for i, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue &&
keyVal.(*KeyValue).Key == name &&
keyVal.(*KeyValue).Value == value {
if !gotIt {
gotIt = true
} else {
obj.Lines[i] = &Deleted{}
}
}
}
if !gotIt {
obj.appendKey(name, value)
}
}
func (obj *Section) SetKey(name, value string) error {
gotIt := false
for i, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue &&
keyVal.(*KeyValue).Key == name {
if !gotIt {
keyVal.(*KeyValue).Value = value
gotIt = true
} else {
obj.Lines[i] = &Deleted{}
}
}
}
if !gotIt {
if createIfNotExist() {
obj.appendKey(name, value)
} else {
return errors.New("Parameter not found: " + name)
}
}
return nil
}

25
types/trash.go Normal file
View File

@ -0,0 +1,25 @@
package types
type Trash struct {
Value string
}
func (obj *Trash) Full() string {
return obj.Value
}
func (obj *Trash) Line() string {
return obj.Value
}
func (obj *Trash) Fulln() string {
return obj.Full() + endOfLine
}
func (obj *Trash) Type() TElement {
return TTrash
}
func (obj *Trash) Indent() string {
return ""
}

46
writer.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"bufio"
"io/ioutil"
"os"
"multini/types"
)
func replaceOriginal(oldFile, newFile string) error {
err := os.Remove(oldFile)
if err == nil {
err = os.Rename(newFile, oldFile)
}
return err
}
func iniWrite(filename string, ini *types.Ini) error {
tmpFile, err := ioutil.TempFile(os.TempDir(), "multini")
if err == nil {
datawriter := bufio.NewWriter(tmpFile)
_, err = datawriter.WriteString(ini.Full())
if err == nil {
err = datawriter.Flush()
tmpFile.Close()
if err == nil {
err = replaceOriginal(filename, tmpFile.Name())
}
}
}
return err
}
func iniWriteInplace(filename string, ini *types.Ini) error {
targetFile, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644)
if err == nil {
datawriter := bufio.NewWriter(targetFile)
_, err = datawriter.WriteString(ini.Full())
if err == nil {
err = datawriter.Flush()
targetFile.Close()
}
}
return err
}