16 Commits
0.1 ... 0.4.0

Author SHA1 Message Date
c258e6096f refactor: improved line parser
- improved support for C-style comments;
- add support for keys with square brackets;
- slightly improved speed.
2020-11-09 17:03:57 +03:00
a5976526ae add spec for src.rpm 2020-11-03 18:26:45 +03:00
3d467bd979 Merge branch 'master' of https://github.com/GenZmeY/multini 2020-11-03 17:36:28 +03:00
28fe0bda17 update .gitignore 2020-11-03 17:36:03 +03:00
25f6b1537d Create README.md 2020-11-03 17:29:09 +03:00
287547b69e add github actions workflow 2020-11-03 17:11:59 +03:00
f4934225b6 Ah shit, here we go again 2020-11-03 13:14:35 +03:00
69be8312f8 add C-style comment support 2020-11-03 11:20:24 +03:00
f083dcd3d8 enable go modules 2020-11-03 11:19:37 +03:00
44e955a047 change project structure 2020-11-03 10:47:19 +03:00
c76140ffe1 release: 0.2.3
- fixed file write for the '--inplace' option
2020-04-30 08:15:26 +03:00
a89e63f19f fix: file write for the '--inplace' option
removes tails for --inplace in case
the source file is larger than the modified one
2020-04-30 08:09:53 +03:00
0ea49ff1ad release: 0.2.2
- stop parsing options when pos args started
- add sections with newline before
2020-04-29 11:38:02 +03:00
325c6c25d7 release: 0.2.1
fix "rename invalid cross-device link"
2020-04-29 05:31:19 +03:00
805813201e docs: update README 2020-04-27 14:37:38 +03:00
702cd21256 release: 0.2
- fix inplace arg
- follow symlinks by default
- reverse flag
- quiet flag
- try chown/chmod on unix
2020-04-27 14:35:10 +03:00
70 changed files with 564 additions and 191 deletions

118
.github/workflows/binary-release.yml vendored Normal file
View File

@ -0,0 +1,118 @@
name: binary release
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3)
- name: checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.13.0'
- name: Build
run: make -j $(nproc) compile VERSION=${{ steps.get_version.outputs.VERSION }}
- name: create release
id: create_release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_version.outputs.VERSION }}
release_name: Release ${{ steps.get_version.outputs.VERSION }}
draft: false
prerelease: false
- name: darwin-386
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-darwin-386
asset_name: ${{ github.event.repository.name }}-darwin-386
asset_content_type: application/octet-stream
- name: darwin-amd64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-darwin-amd64
asset_name: ${{ github.event.repository.name }}-darwin-amd64
asset_content_type: application/octet-stream
- name: freebsd-386
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-freebsd-386
asset_name: ${{ github.event.repository.name }}-freebsd-386
asset_content_type: application/octet-stream
- name: freebsd-amd64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-freebsd-amd64
asset_name: ${{ github.event.repository.name }}-freebsd-amd64
asset_content_type: application/octet-stream
- name: linux-386
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-linux-386
asset_name: ${{ github.event.repository.name }}-linux-386
asset_content_type: application/octet-stream
- name: linux-amd64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-linux-amd64
asset_name: ${{ github.event.repository.name }}-linux-amd64
asset_content_type: application/octet-stream
- name: windows-386
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-windows-386.exe
asset_name: ${{ github.event.repository.name }}-windows-386.exe
asset_content_type: application/octet-stream
- name: windows-amd64
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./bin/${{ github.event.repository.name }}-windows-amd64.exe
asset_name: ${{ github.event.repository.name }}-windows-amd64.exe
asset_content_type: application/octet-stream

23
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.13.0'
- name: Build
run: make -j $(nproc)

23
.gitignore vendored
View File

@ -1,20 +1,3 @@
# Binaries for programs and plugins /bin/
bin/* /test/data/out_ini/
multini /cmd/multini/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/

View File

@ -1,21 +1,30 @@
NAME=multini NAME = multini
VERSION=0.1 VERSION = dev_$(shell date +%F_%T)
GOCMD=go GOCMD = go
LDFLAGS:="$(LDFLAGS) -X 'main.Version=$(VERSION)'" LDFLAGS := "$(LDFLAGS) -X 'main.Version=$(VERSION)'"
GOBUILD=$(GOCMD) build -ldflags=$(LDFLAGS) GOBUILD = $(GOCMD) build -ldflags=$(LDFLAGS)
SRCMAIN=. SRCMAIN = ./cmd/$(NAME)
BINDIR=bin SRCDOC = ./doc
BIN=$(BINDIR)/$(NAME) SRCTEST = ./test
README=README BINDIR = bin
LICENSE=LICENSE BIN = $(BINDIR)/$(NAME)
TEST=./run_test.sh README = $(SRCDOC)/README
PREFIX=/usr LICENSE = LICENSE
TEST = $(SRCTEST)/run_test.sh
PREFIX = /usr
.PHONY: all prep doc build check-build freebsd-386 darwin-386 linux-386 windows-386 freebsd-amd64 darwin-amd64 linux-amd64 windows-amd64 compile install check-install uninstall clean test
all: build all: build
prep: clean prep: clean
go mod init; go mod tidy
mkdir $(BINDIR) mkdir $(BINDIR)
doc: check-build
test -d $(SRCDOC) || mkdir $(SRCDOC)
$(BIN) --help > $(README)
build: prep build: prep
$(GOBUILD) -o $(BIN) $(SRCMAIN) $(GOBUILD) -o $(BIN) $(SRCMAIN)
@ -48,7 +57,7 @@ windows-amd64: prep
compile: freebsd-386 darwin-386 linux-386 windows-386 freebsd-amd64 darwin-amd64 linux-amd64 windows-amd64 compile: freebsd-386 darwin-386 linux-386 windows-386 freebsd-amd64 darwin-amd64 linux-amd64 windows-amd64
install: check-build install: check-build doc
install -m 755 -d $(PREFIX)/bin/ install -m 755 -d $(PREFIX)/bin/
install -m 755 $(BIN) $(PREFIX)/bin/ install -m 755 $(BIN) $(PREFIX)/bin/
install -m 755 -d $(PREFIX)/share/licenses/$(NAME)/ install -m 755 -d $(PREFIX)/share/licenses/$(NAME)/
@ -71,3 +80,4 @@ clean:
test: check-build test: check-build
$(TEST) $(BIN) $(TEST) $(BIN)

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# multini
it's like crudini but for ini files with duplicate parameter names

View File

@ -1,8 +1,8 @@
package main package main
import ( import (
"multini/output" "multini/internal/output"
"multini/types" "multini/internal/types"
) )
func chk() int { func chk() int {
@ -22,7 +22,7 @@ func chk() int {
func add(ini *types.Ini) error { func add(ini *types.Ini) error {
if ArgKeyIsSet { if ArgKeyIsSet {
return ini.AddKey(ArgSection, ArgKey, ArgValue) return ini.AddKey(ArgSection, ArgKey, ArgValue, ArgReverse)
} else { } else {
ini.AddSection(ArgSection) ini.AddSection(ArgSection)
return nil return nil

View File

@ -4,8 +4,8 @@ import (
"errors" "errors"
"os" "os"
"multini/output" "multini/internal/output"
"multini/types" "multini/internal/types"
"github.com/juju/gnuflag" "github.com/juju/gnuflag"
) )
@ -23,6 +23,8 @@ var (
ArgUnix bool ArgUnix bool
ArgHelp bool ArgHelp bool
ArgExisting bool ArgExisting bool
ArgReverse bool
ArgQuiet bool
ArgOutput string ArgOutput string
ArgFile string ArgFile string
@ -40,14 +42,15 @@ func printHelp() {
output.Println("") output.Println("")
output.Println("Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]") output.Println("Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]")
output.Println("Actions:") output.Println("Actions:")
output.Println(" -g, --get get values for a given combination of parameters.") 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(" -s, --set Set values for a given combination of parameters.")
output.Println(" -a, --add add 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(" -d, --del Delete the given combination of parameters.")
output.Println(" -c, --chk display parsing errors for the specified file.") output.Println(" -c, --chk Display parsing errors for the specified file.")
output.Println("") output.Println("")
output.Println("Options:") output.Println("Options:")
output.Println(" -e, --existing For --set and --del, fail if item is missing.") output.Println(" -e, --existing For --set and --del, fail if item is missing.")
output.Println(" -r, --reverse For --add, adds an item to the top of the section")
output.Println(" -i, --inplace Lock and write files in place.") output.Println(" -i, --inplace Lock and write files in place.")
output.Println(" This is not atomic but has less restrictions") output.Println(" This is not atomic but has less restrictions")
output.Println(" than the default replacement method.") output.Println(" than the default replacement method.")
@ -55,6 +58,7 @@ func printHelp() {
// output.Println(" -v, --verbose Indicate on stderr if changes were made") // output.Println(" -v, --verbose Indicate on stderr if changes were made")
output.Println(" -u, --unix Use LF as end of line") output.Println(" -u, --unix Use LF as end of line")
output.Println(" -w, --windows Use CRLF as end of line") output.Println(" -w, --windows Use CRLF as end of line")
output.Println(" -q, --quiet Suppress all normal output")
output.Println(" -h, --help Write this help to stdout") output.Println(" -h, --help Write this help to stdout")
output.Println(" --version Write version to stdout") output.Println(" --version Write version to stdout")
} }
@ -74,14 +78,18 @@ func init() {
gnuflag.BoolVar(&ArgDel, "d", false, "") gnuflag.BoolVar(&ArgDel, "d", false, "")
gnuflag.BoolVar(&ArgChk, "chk", false, "") gnuflag.BoolVar(&ArgChk, "chk", false, "")
gnuflag.BoolVar(&ArgChk, "c", false, "") gnuflag.BoolVar(&ArgChk, "c", false, "")
gnuflag.BoolVar(&ArgDel, "inplace", false, "") gnuflag.BoolVar(&ArgInplace, "inplace", false, "")
gnuflag.BoolVar(&ArgDel, "i", false, "") gnuflag.BoolVar(&ArgInplace, "i", false, "")
gnuflag.BoolVar(&ArgUnix, "unix", false, "") gnuflag.BoolVar(&ArgUnix, "unix", false, "")
gnuflag.BoolVar(&ArgUnix, "u", false, "") gnuflag.BoolVar(&ArgUnix, "u", false, "")
gnuflag.BoolVar(&ArgWindows, "windows", false, "") gnuflag.BoolVar(&ArgWindows, "windows", false, "")
gnuflag.BoolVar(&ArgWindows, "w", false, "") gnuflag.BoolVar(&ArgWindows, "w", false, "")
gnuflag.BoolVar(&ArgReverse, "reverse", false, "")
gnuflag.BoolVar(&ArgReverse, "r", false, "")
gnuflag.BoolVar(&ArgExisting, "existing", false, "") gnuflag.BoolVar(&ArgExisting, "existing", false, "")
gnuflag.BoolVar(&ArgExisting, "e", false, "") gnuflag.BoolVar(&ArgExisting, "e", false, "")
gnuflag.BoolVar(&ArgQuiet, "quiet", false, "")
gnuflag.BoolVar(&ArgQuiet, "q", false, "")
gnuflag.BoolVar(&ArgVerbose, "verbose", false, "") gnuflag.BoolVar(&ArgVerbose, "verbose", false, "")
gnuflag.BoolVar(&ArgVerbose, "v", false, "") gnuflag.BoolVar(&ArgVerbose, "v", false, "")
gnuflag.StringVar(&ArgOutput, "output", "", "") gnuflag.StringVar(&ArgOutput, "output", "", "")
@ -92,7 +100,7 @@ func init() {
} }
func parseArgs() error { func parseArgs() error {
gnuflag.Parse(true) gnuflag.Parse(false)
// info // info
switch { switch {
@ -117,6 +125,7 @@ func parseArgs() error {
// Output settings // Output settings
output.SetEndOfLineNative() output.SetEndOfLineNative()
output.SetVerbose(ArgVerbose) output.SetVerbose(ArgVerbose)
output.SetQuiet(ArgQuiet)
// Positional Args // Positional Args
for i := 0; i < 4 && i < gnuflag.NArg(); i++ { for i := 0; i < 4 && i < gnuflag.NArg(); i++ {

View File

@ -1,11 +1,10 @@
package main package main
import ( import (
"fmt"
"os" "os"
"multini/output" "multini/internal/output"
"multini/types" "multini/internal/types"
) )
const ( const (
@ -37,7 +36,7 @@ func main() {
ini, err = iniRead(ArgFile) ini, err = iniRead(ArgFile)
if err != nil { if err != nil {
fmt.Println(err) output.Errorln(err)
os.Exit(EXIT_FILE_READ_ERR) os.Exit(EXIT_FILE_READ_ERR)
} }

View File

@ -7,8 +7,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"multini/output" "multini/internal/output"
"multini/types" "multini/internal/types"
) )
var ( var (
@ -22,34 +22,88 @@ var (
NgValuePrefix string = `value_prefix` NgValuePrefix string = `value_prefix`
NgValuePostfix string = `value_postfix` NgValuePostfix string = `value_postfix`
NgComment string = `comment` NgComment string = `comment`
NgCommentPrefix string = `comment_prefix` NgData string = `data`
RxBodyPrefix string = `(?P<` + NgPrefix + `>\s+)?` RxEmpty string = `^(?P<` + NgPrefix + `>\s+)?$`
RxSectionName string = `\[(?P<` + NgSection + `>.+)\]` RxSection string = `^(?P<` + NgPrefix + `>\s+)?\[(?P<` + NgSection + `>[^\]]+)\](?P<` + NgPostifx + `>\s+)?$`
RxKey string = `(?P<` + NgKey + `>(?:[^;#=]+[^\s=;#]|[^;#=]))?` RxKey string = `^(?P<` + NgPrefix + `>\s+)?(?P<` + NgKey + `>.*[^\s]+)(?P<` + NgKeyPostfix + `>\s+)?$`
RxKeyPostfix string = `(?P<` + NgKeyPostfix + `>\s+)?` RxValue string = `^(?P<` + NgValuePrefix + `>\s+)?(?P<` + NgValue + `>.*[^\s])(?P<` + NgValuePostfix + `>\s+)?$`
RxValuePrefix string = `(?P<` + NgValuePrefix + `>\s+)?`
RxValue string = `(?P<` + NgValue + `>(?:[^;#]+[^\s;#]|[^;#]))?` RxEmptyCompile *regexp.Regexp = regexp.MustCompile(RxEmpty)
RxValuePostfix string = `(?P<` + NgValuePostfix + `>\s+)?` RxSectionCompile *regexp.Regexp = regexp.MustCompile(RxSection)
RxKeyVal string = RxKey + RxKeyPostfix + `=` + RxValuePrefix + RxValue + RxValuePostfix RxKeyCompile *regexp.Regexp = regexp.MustCompile(RxKey)
RxBody string = `(?:` + RxSectionName + `|` + RxKeyVal + `)?` RxValueCompile *regexp.Regexp = regexp.MustCompile(RxValue)
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 { func parse(str string) map[string]string {
match := rx.FindStringSubmatch(str) var result map[string]string = make(map[string]string)
result := make(map[string]string) var data string
for i, name := range rx.SubexpNames() {
if i != 0 && name != "" && i <= len(match) { data, result[NgComment] = getDataComment(str)
result[name] = match[i]
} if data != "" {
findNamedGroups(data, RxEmptyCompile, &result)
} }
if result[NgPrefix] != "" {
return result return result
}
findNamedGroups(data, RxSectionCompile, &result)
if result[NgSection] == "" && data != "" {
keyPart, valPart := getKeyValue(data)
findNamedGroups(keyPart, RxKeyCompile, &result)
findNamedGroups(valPart, RxValueCompile, &result)
}
return result
}
func findNamedGroups(str string, Rx *regexp.Regexp, result *map[string]string) {
match := Rx.FindStringSubmatch(str)
for i, name := range Rx.SubexpNames() {
if i != 0 && name != "" && i <= len(match) {
(*result)[name] = match[i]
}
}
}
func getDataComment(str string) (string, string) {
var indexes []int
var commentIndex int = -1
indexes = append(indexes, strings.Index(str, "//"))
indexes = append(indexes, strings.Index(str, "#"))
indexes = append(indexes, strings.Index(str, ";"))
for _, index := range indexes {
if commentIndex == -1 {
if index != -1 {
commentIndex = index
}
} else {
if index != -1 {
if commentIndex > index {
commentIndex = index
}
}
}
}
if commentIndex == -1 {
return str, ""
} else {
return str[:commentIndex], str[commentIndex:]
}
}
func getKeyValue(data string) (string, string) {
index := strings.Index(data, "=")
if index != -1 {
return data[:index], data[index+1:]
}
return "", ""
} }
func debugMap(el map[string]string) string { func debugMap(el map[string]string) string {
@ -61,14 +115,14 @@ func debugMap(el map[string]string) string {
} }
func appendLine(ini *types.Ini, line string) error { func appendLine(ini *types.Ini, line string) error {
elements := rxParse(RxCompiled, line) // elements := rxParse(line)
elements := parse(line)
switch { switch {
case elements[NgSection] != "": case elements[NgSection] != "":
var newSection types.Section var newSection types.Section
newSection.Name = elements[NgSection] newSection.Name = elements[NgSection]
newSection.Prefix = elements[NgPrefix] newSection.Prefix = elements[NgPrefix]
newSection.Postfix = elements[NgPostifx] newSection.Postfix = elements[NgPostifx]
newSection.Comment.Prefix = elements[NgCommentPrefix]
newSection.Comment.Value = elements[NgComment] newSection.Comment.Value = elements[NgComment]
if newSection.Line() == line { if newSection.Line() == line {
ini.Sections = append(ini.Sections, &newSection) ini.Sections = append(ini.Sections, &newSection)
@ -87,7 +141,6 @@ func appendLine(ini *types.Ini, line string) error {
newKeyValue.PrefixValue = elements[NgValuePrefix] newKeyValue.PrefixValue = elements[NgValuePrefix]
newKeyValue.PostfixValue = elements[NgValuePostfix] newKeyValue.PostfixValue = elements[NgValuePostfix]
newKeyValue.Comment.Value = elements[NgComment] newKeyValue.Comment.Value = elements[NgComment]
newKeyValue.Comment.Prefix = elements[NgCommentPrefix]
if newKeyValue.Line() == line { if newKeyValue.Line() == line {
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newKeyValue) ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newKeyValue)
return nil return nil
@ -98,8 +151,8 @@ func appendLine(ini *types.Ini, line string) error {
} }
case elements[NgComment] != "": case elements[NgComment] != "":
var newComment types.Comment var newComment types.Comment
newComment.Prefix = elements[NgPrefix] + elements[NgCommentPrefix]
newComment.Value = elements[NgComment] newComment.Value = elements[NgComment]
newComment.Prefix = elements[NgPrefix]
if newComment.Line() == line { if newComment.Line() == line {
ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newComment) ini.Sections[len(ini.Sections)-1].(*types.Section).Lines = append(ini.Sections[len(ini.Sections)-1].(*types.Section).Lines, &newComment)
return nil return nil

17
cmd/multini/stat_unix.go Normal file
View File

@ -0,0 +1,17 @@
// +build !windows
package main
import (
"os"
"syscall"
)
func GetUidGid(info os.FileInfo) (int, int) {
stat, ok := info.Sys().(*syscall.Stat_t)
if ok {
return int(stat.Uid), int(stat.Gid)
} else {
return -1, -1
}
}

View File

@ -0,0 +1,11 @@
// +build windows
package main
import (
"os"
)
func GetUidGid(info os.FileInfo) (int, int) {
return -1, -1
}

127
cmd/multini/writer.go Normal file
View File

@ -0,0 +1,127 @@
package main
import (
"bufio"
"io"
"io/ioutil"
"os"
"path/filepath"
"multini/internal/types"
)
// Source: https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b
/*
GoLang: os.Rename() give error "invalid cross-device link" for Docker container with Volumes.
MoveFile(source, destination) will work moving file between folders
*/
func tryMoveFile(sourcePath, destPath string) error {
inputFile, err := os.Open(sourcePath)
if err != nil {
return err
}
outputFile, err := os.Create(destPath)
if err != nil {
inputFile.Close()
return err
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return err
}
// The copy was successful, so now delete the original file
err = os.Remove(sourcePath)
if err != nil {
return err
}
return nil
}
func tryRemoveRenameFile(sourcePath, destPath string) bool {
err := os.Remove(destPath)
if err != nil {
return false
}
err = os.Rename(sourcePath, destPath)
if err != nil {
return false
}
return true
}
func replaceOriginal(oldFile, newFile string) error {
realOldFile, err := filepath.EvalSymlinks(oldFile)
if err != nil {
return err
}
infoOldFile, err := os.Stat(realOldFile)
if err != nil {
return err
}
mode := infoOldFile.Mode()
var uid, gid int = GetUidGid(infoOldFile)
if !tryRemoveRenameFile(newFile, realOldFile) {
err = tryMoveFile(newFile, realOldFile)
if err != nil {
return err
}
}
err = os.Chmod(realOldFile, mode)
if err != nil {
return err
}
// try to restore original uid/gid
// don't worry if we can't
os.Chown(realOldFile, uid, gid)
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 {
realfilename, err := filepath.EvalSymlinks(filename)
mode := os.FileMode(int(0644))
if os.IsNotExist(err) {
realfilename = filename
} else if err != nil {
return err
} else {
info, err := os.Stat(realfilename)
if err != nil {
return err
}
mode = info.Mode()
}
targetFile, err := os.OpenFile(realfilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err == nil {
datawriter := bufio.NewWriter(targetFile)
_, err = datawriter.WriteString(ini.Full())
if err == nil {
err = datawriter.Flush()
targetFile.Close()
}
}
return err
}

View File

@ -2,21 +2,21 @@ A utility for manipulating ini files with duplicate keys
Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value] Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]
Actions: Actions:
-g, --get get values for a given combination of parameters. -g, --get Get values for a given combination of parameters.
-s, --set set 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. -a, --add Add values for a given combination of parameters.
-d, --del delete the given combination of parameters. -d, --del Delete the given combination of parameters.
-c, --chk display parsing errors for the specified file. -c, --chk Display parsing errors for the specified file.
Options: Options:
-e, --existing For --set and --del, fail if item is missing. -e, --existing For --set and --del, fail if item is missing.
-r, --reverse For --add, adds an item to the top of the section
-i, --inplace Lock and write files in place. -i, --inplace Lock and write files in place.
This is not atomic but has less restrictions This is not atomic but has less restrictions
than the default replacement method. than the default replacement method.
-o, --output FILE Write output to FILE instead. '-' means stdout -o, --output FILE Write output to FILE instead. '-' means stdout
-u, --unix Use LF as end of line -u, --unix Use LF as end of line
-w, --windows Use CRLF as end of line -w, --windows Use CRLF as end of line
-q, --quiet Suppress all normal output
-h, --help Write this help to stdout -h, --help Write this help to stdout
--version Write version to stdout --version Write version to stdout
2020 (c) GenZmeY

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module multini
go 1.13
require github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=

View File

@ -16,6 +16,14 @@ var (
verbose *log.Logger = devNull verbose *log.Logger = devNull
) )
func SetQuiet(enabled bool) {
if enabled {
stdout = devNull
stderr = devNull
verbose = devNull
}
}
func SetVerbose(enabled bool) { func SetVerbose(enabled bool) {
if enabled { if enabled {
verbose = stderr verbose = stderr

View File

@ -89,6 +89,14 @@ func (obj *Ini) GetKeyVal(section, key, value string) error {
func (obj *Ini) AddSection(section string) *Section { func (obj *Ini) AddSection(section string) *Section {
sect, err := obj.FindSection(section) sect, err := obj.FindSection(section)
if err != nil { if err != nil {
sectSize := len(obj.Sections)
if sectSize > 1 {
prevSect := obj.Sections[sectSize-1].(*Section)
lineSize := len(prevSect.Lines)
if lineSize == 0 || lineSize > 0 && prevSect.Lines[lineSize-1].Type() != TEmptyLine {
obj.Sections[sectSize-1].(*Section).Lines = append(obj.Sections[sectSize-1].(*Section).Lines, &EmptyLine{})
}
}
var newSection Section var newSection Section
newSection.Name = section newSection.Name = section
newSection.Prefix = obj.Sections[len(obj.Sections)-1].Indent() newSection.Prefix = obj.Sections[len(obj.Sections)-1].Indent()
@ -102,7 +110,7 @@ func (obj *Ini) SetSection(section string) *Section {
return obj.AddSection(section) return obj.AddSection(section)
} }
func (obj *Ini) AddKey(section, key, value string) error { func (obj *Ini) AddKey(section, key, value string, reverse bool) error {
sect, err := obj.FindSection(section) sect, err := obj.FindSection(section)
if err != nil { if err != nil {
if createIfNotExist() { if createIfNotExist() {
@ -111,7 +119,7 @@ func (obj *Ini) AddKey(section, key, value string) error {
return err return err
} }
} }
sect.AddKey(key, value) sect.AddKey(key, value, reverse)
return nil return nil
} }

View File

@ -111,11 +111,25 @@ func (obj *Section) GetKeyVal(name, value string) error {
return errors.New("Parameter:Value not found: " + name + ":" + value) return errors.New("Parameter:Value not found: " + name + ":" + value)
} }
func (obj *Section) appendKey(name, value string) { func (obj *Section) appendKey(name, value string, reverse bool) {
var newKeyValue KeyValue var newKeyValue KeyValue
var replaceIndex int = -1 var replaceIndex int = -1
newKeyValue.Key = name newKeyValue.Key = name
newKeyValue.Value = value newKeyValue.Value = value
if reverse {
// for right indent and tabs
for i := 0; i < len(obj.Lines); 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
}
}
obj.Lines = append([]Element{&newKeyValue}, obj.Lines...)
} else {
// replace first emptyline // replace first emptyline
for i := len(obj.Lines) - 1; i >= 0; i-- { for i := len(obj.Lines) - 1; i >= 0; i-- {
if obj.Lines[i].Type() == TEmptyLine { if obj.Lines[i].Type() == TEmptyLine {
@ -141,9 +155,10 @@ func (obj *Section) appendKey(name, value string) {
obj.Lines = append(obj.Lines, obj.Lines[replaceIndex]) obj.Lines = append(obj.Lines, obj.Lines[replaceIndex])
obj.Lines[replaceIndex] = &newKeyValue obj.Lines[replaceIndex] = &newKeyValue
} }
}
} }
func (obj *Section) AddKey(name, value string) { func (obj *Section) AddKey(name, value string, reverse bool) {
gotIt := false gotIt := false
for i, keyVal := range obj.Lines { for i, keyVal := range obj.Lines {
if keyVal.Type() == TKeyValue && if keyVal.Type() == TKeyValue &&
@ -157,7 +172,7 @@ func (obj *Section) AddKey(name, value string) {
} }
} }
if !gotIt { if !gotIt {
obj.appendKey(name, value) obj.appendKey(name, value, reverse)
} }
} }
@ -176,7 +191,7 @@ func (obj *Section) SetKey(name, value string) error {
} }
if !gotIt { if !gotIt {
if createIfNotExist() { if createIfNotExist() {
obj.appendKey(name, value) obj.appendKey(name, value, false)
} else { } else {
return errors.New("Parameter not found: " + name) return errors.New("Parameter not found: " + name)
} }

View File

@ -1,6 +1,6 @@
# comment # comment
; comment again ; comment again
// comment with indent
DefKey1 = Some Value1 DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces! DefKey3=NoSpaces!
@ -10,7 +10,7 @@ DefKey3=NoSpaces!
Key2 = 2 Key2 = 2
Key3 = 3 Key3 = 3
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3
@ -17,4 +17,5 @@ DefKey3=NoSpaces!
[SectionWithIndent] [SectionWithIndent]
Key=Value Key=Value
[SectionWithoutNewLineBefore] [SectionWithoutNewLineBefore]
[NewSection] [NewSection]

View File

@ -8,7 +8,7 @@ DefKey3=NoSpaces!
[SimpleSection] # Comment For Section [SimpleSection] # Comment For Section
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
[SectionWithIndent] [SectionWithIndent]
Key=Value Key=Value

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -0,0 +1 @@
skip/skip

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = onlyone Key = onlyone
[SectionWithIndent] [SectionWithIndent]

View File

@ -1,6 +1,6 @@
# comment # comment
; comment again ; comment again
// comment with indent
DefKey1 = Some Value1 DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces! DefKey3=NoSpaces!
@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -0,0 +1,19 @@
# comment
; comment again
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!
[Slashes/Test] // Comment For Section
./Dir1/File = skip/skip // comment
Key2 = 2
[MultipleKeySection] // C style comment
Key = 1
Key = 2
Key = 3
[SectionWithIndent]
Key=Value
[SectionWithoutNewLineBefore]

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

@ -9,7 +9,7 @@ DefKey3=NoSpaces!
Key1 = 1 Key1 = 1
Key2 = 2 Key2 = 2
[MultipleKeySection] [MultipleKeySection] // C style comment
Key = 1 Key = 1
Key = 2 Key = 2
Key = 3 Key = 3

View File

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

View File

@ -1,10 +1,11 @@
#!/bin/bash #!/bin/bash
DEF='\e[0m'; BLD='\e[1m'; RED='\e[31m'; GRN='\e[32m'; WHT='\e[97m' DEF='\e[0m'; BLD='\e[1m'; RED='\e[31m'; GRN='\e[32m'; WHT='\e[97m'
ScriptFullname=$(readlink -e "$0") ScriptFullname=$(readlink -e "$0")
ScriptName=$(echo "$ScriptFullname" | awk -F '/' '{print $NF;}') ScriptName=$(echo "$ScriptFullname" | awk -F '/' '{print $NF;}')
ScriptDir=$(dirname "$ScriptFullname") ScriptDir=$(dirname "$ScriptFullname")
TestDir="$ScriptDir/tests" TestDir="$ScriptDir/data"
Multini=$(readlink -e "$1") Multini=$(readlink -e "$1")
if [[ -z "$Multini" ]]; then if [[ -z "$Multini" ]]; then

View File

@ -1,46 +0,0 @@
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
}