4 Commits
0.1 ... 0.2.2

Author SHA1 Message Date
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
12 changed files with 201 additions and 52 deletions

View File

@ -1,5 +1,5 @@
NAME=multini NAME=multini
VERSION=0.1 VERSION=0.2.2
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)

14
README
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

View File

@ -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

25
args.go
View File

@ -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,7 +1,6 @@
package main package main
import ( import (
"fmt"
"os" "os"
"multini/output" "multini/output"
@ -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

@ -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

17
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
}
}

11
stat_windows.go Normal file
View File

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

View File

@ -17,4 +17,5 @@ DefKey3=NoSpaces!
[SectionWithIndent] [SectionWithIndent]
Key=Value Key=Value
[SectionWithoutNewLineBefore] [SectionWithoutNewLineBefore]
[NewSection] [NewSection]

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,39 +111,54 @@ 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
// replace first emptyline if reverse {
for i := len(obj.Lines) - 1; i >= 0; i-- { // for right indent and tabs
if obj.Lines[i].Type() == TEmptyLine { for i := 0; i < len(obj.Lines); i++ {
replaceIndex = i if obj.Lines[i].Type() == TKeyValue {
} else { template := obj.Lines[i].(*KeyValue)
break newKeyValue.PrefixKey = template.PrefixKey
newKeyValue.PostfixKey = template.PostfixKey
newKeyValue.PrefixValue = template.PrefixValue
newKeyValue.PostfixValue = template.PostfixValue
break
}
} }
} obj.Lines = append([]Element{&newKeyValue}, obj.Lines...)
// 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 { } else {
obj.Lines = append(obj.Lines, obj.Lines[replaceIndex]) // replace first emptyline
obj.Lines[replaceIndex] = &newKeyValue 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) { 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

@ -2,17 +2,85 @@ package main
import ( import (
"bufio" "bufio"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"multini/types" "multini/types"
) )
func replaceOriginal(oldFile, newFile string) error { // Source: https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b
err := os.Remove(oldFile) /*
if err == nil { GoLang: os.Rename() give error "invalid cross-device link" for Docker container with Volumes.
err = os.Rename(newFile, oldFile) 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 return err
} }
@ -33,7 +101,20 @@ func iniWrite(filename string, ini *types.Ini) error {
} }
func iniWriteInplace(filename string, ini *types.Ini) error { func iniWriteInplace(filename string, ini *types.Ini) error {
targetFile, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644) 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, mode)
if err == nil { if err == nil {
datawriter := bufio.NewWriter(targetFile) datawriter := bufio.NewWriter(targetFile)
_, err = datawriter.WriteString(ini.Full()) _, err = datawriter.WriteString(ini.Full())