Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
c76140ffe1 | |||
a89e63f19f | |||
0ea49ff1ad | |||
325c6c25d7 | |||
805813201e | |||
702cd21256 |
2
Makefile
2
Makefile
@ -1,5 +1,5 @@
|
||||
NAME=multini
|
||||
VERSION=0.1
|
||||
VERSION=0.2.3
|
||||
GOCMD=go
|
||||
LDFLAGS:="$(LDFLAGS) -X 'main.Version=$(VERSION)'"
|
||||
GOBUILD=$(GOCMD) build -ldflags=$(LDFLAGS)
|
||||
|
14
README
14
README
@ -2,21 +2,21 @@ 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.
|
||||
-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.
|
||||
-r, --reverse For --add, adds an item to the top of the section
|
||||
-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
|
||||
-q, --quiet Suppress all normal output
|
||||
-h, --help Write this help to stdout
|
||||
--version Write version to stdout
|
||||
|
||||
2020 (c) GenZmeY
|
||||
|
@ -22,7 +22,7 @@ func chk() int {
|
||||
|
||||
func add(ini *types.Ini) error {
|
||||
if ArgKeyIsSet {
|
||||
return ini.AddKey(ArgSection, ArgKey, ArgValue)
|
||||
return ini.AddKey(ArgSection, ArgKey, ArgValue, ArgReverse)
|
||||
} else {
|
||||
ini.AddSection(ArgSection)
|
||||
return nil
|
||||
|
25
args.go
25
args.go
@ -23,6 +23,8 @@ var (
|
||||
ArgUnix bool
|
||||
ArgHelp bool
|
||||
ArgExisting bool
|
||||
ArgReverse bool
|
||||
ArgQuiet bool
|
||||
ArgOutput string
|
||||
|
||||
ArgFile string
|
||||
@ -40,14 +42,15 @@ func printHelp() {
|
||||
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(" -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(" -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(" This is not atomic but has less restrictions")
|
||||
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(" -u, --unix Use LF 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(" --version Write version to stdout")
|
||||
}
|
||||
@ -74,14 +78,18 @@ func init() {
|
||||
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(&ArgInplace, "inplace", false, "")
|
||||
gnuflag.BoolVar(&ArgInplace, "i", false, "")
|
||||
gnuflag.BoolVar(&ArgUnix, "unix", false, "")
|
||||
gnuflag.BoolVar(&ArgUnix, "u", false, "")
|
||||
gnuflag.BoolVar(&ArgWindows, "windows", false, "")
|
||||
gnuflag.BoolVar(&ArgWindows, "w", false, "")
|
||||
gnuflag.BoolVar(&ArgReverse, "reverse", false, "")
|
||||
gnuflag.BoolVar(&ArgReverse, "r", false, "")
|
||||
gnuflag.BoolVar(&ArgExisting, "existing", false, "")
|
||||
gnuflag.BoolVar(&ArgExisting, "e", false, "")
|
||||
gnuflag.BoolVar(&ArgQuiet, "quiet", false, "")
|
||||
gnuflag.BoolVar(&ArgQuiet, "q", false, "")
|
||||
gnuflag.BoolVar(&ArgVerbose, "verbose", false, "")
|
||||
gnuflag.BoolVar(&ArgVerbose, "v", false, "")
|
||||
gnuflag.StringVar(&ArgOutput, "output", "", "")
|
||||
@ -92,7 +100,7 @@ func init() {
|
||||
}
|
||||
|
||||
func parseArgs() error {
|
||||
gnuflag.Parse(true)
|
||||
gnuflag.Parse(false)
|
||||
|
||||
// info
|
||||
switch {
|
||||
@ -117,6 +125,7 @@ func parseArgs() error {
|
||||
// Output settings
|
||||
output.SetEndOfLineNative()
|
||||
output.SetVerbose(ArgVerbose)
|
||||
output.SetQuiet(ArgQuiet)
|
||||
|
||||
// Positional Args
|
||||
for i := 0; i < 4 && i < gnuflag.NArg(); i++ {
|
||||
|
3
main.go
3
main.go
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"multini/output"
|
||||
@ -37,7 +36,7 @@ func main() {
|
||||
|
||||
ini, err = iniRead(ArgFile)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
output.Errorln(err)
|
||||
os.Exit(EXIT_FILE_READ_ERR)
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,14 @@ var (
|
||||
verbose *log.Logger = devNull
|
||||
)
|
||||
|
||||
func SetQuiet(enabled bool) {
|
||||
if enabled {
|
||||
stdout = devNull
|
||||
stderr = devNull
|
||||
verbose = devNull
|
||||
}
|
||||
}
|
||||
|
||||
func SetVerbose(enabled bool) {
|
||||
if enabled {
|
||||
verbose = stderr
|
||||
|
17
stat_unix.go
Normal file
17
stat_unix.go
Normal 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
11
stat_windows.go
Normal file
@ -0,0 +1,11 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetUidGid(info os.FileInfo) (int, int) {
|
||||
return -1, -1
|
||||
}
|
@ -17,4 +17,5 @@ DefKey3=NoSpaces!
|
||||
[SectionWithIndent]
|
||||
Key=Value
|
||||
[SectionWithoutNewLineBefore]
|
||||
|
||||
[NewSection]
|
||||
|
12
types/ini.go
12
types/ini.go
@ -89,6 +89,14 @@ func (obj *Ini) GetKeyVal(section, key, value string) error {
|
||||
func (obj *Ini) AddSection(section string) *Section {
|
||||
sect, err := obj.FindSection(section)
|
||||
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
|
||||
newSection.Name = section
|
||||
newSection.Prefix = obj.Sections[len(obj.Sections)-1].Indent()
|
||||
@ -102,7 +110,7 @@ func (obj *Ini) SetSection(section string) *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)
|
||||
if err != nil {
|
||||
if createIfNotExist() {
|
||||
@ -111,7 +119,7 @@ func (obj *Ini) AddKey(section, key, value string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sect.AddKey(key, value)
|
||||
sect.AddKey(key, value, reverse)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -111,11 +111,25 @@ func (obj *Section) GetKeyVal(name, value string) error {
|
||||
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 replaceIndex int = -1
|
||||
newKeyValue.Key = name
|
||||
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
|
||||
for i := len(obj.Lines) - 1; i >= 0; i-- {
|
||||
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[replaceIndex] = &newKeyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *Section) AddKey(name, value string) {
|
||||
func (obj *Section) AddKey(name, value string, reverse bool) {
|
||||
gotIt := false
|
||||
for i, keyVal := range obj.Lines {
|
||||
if keyVal.Type() == TKeyValue &&
|
||||
@ -157,7 +172,7 @@ func (obj *Section) AddKey(name, value string) {
|
||||
}
|
||||
}
|
||||
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 createIfNotExist() {
|
||||
obj.appendKey(name, value)
|
||||
obj.appendKey(name, value, false)
|
||||
} else {
|
||||
return errors.New("Parameter not found: " + name)
|
||||
}
|
||||
|
91
writer.go
91
writer.go
@ -2,17 +2,85 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"multini/types"
|
||||
)
|
||||
|
||||
func replaceOriginal(oldFile, newFile string) error {
|
||||
err := os.Remove(oldFile)
|
||||
if err == nil {
|
||||
err = os.Rename(newFile, oldFile)
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -33,7 +101,20 @@ func iniWrite(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|os.O_TRUNC, mode)
|
||||
if err == nil {
|
||||
datawriter := bufio.NewWriter(targetFile)
|
||||
_, err = datawriter.WriteString(ini.Full())
|
||||
|
Reference in New Issue
Block a user