diff --git a/Makefile b/Makefile index 5150321..a291400 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ NAME=multini -VERSION=0.1 +VERSION=0.2 GOCMD=go LDFLAGS:="$(LDFLAGS) -X 'main.Version=$(VERSION)'" GOBUILD=$(GOCMD) build -ldflags=$(LDFLAGS) diff --git a/actions.go b/actions.go index 63775c1..1ce773a 100644 --- a/actions.go +++ b/actions.go @@ -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 diff --git a/args.go b/args.go index 2b07e33..61441c2 100644 --- a/args.go +++ b/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", "", "") @@ -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++ { diff --git a/main.go b/main.go index 40e3390..a7db735 100644 --- a/main.go +++ b/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) } diff --git a/output/output.go b/output/output.go index b61f4a2..c4604e3 100644 --- a/output/output.go +++ b/output/output.go @@ -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 diff --git a/stat_unix.go b/stat_unix.go new file mode 100644 index 0000000..d416e98 --- /dev/null +++ b/stat_unix.go @@ -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 + } +} diff --git a/stat_windows.go b/stat_windows.go new file mode 100644 index 0000000..0f08560 --- /dev/null +++ b/stat_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package main + +import ( + "os" +) + +func GetUidGid(info os.FileInfo) (int, int) { + return -1, -1 +} diff --git a/types/ini.go b/types/ini.go index 518810b..10f850b 100644 --- a/types/ini.go +++ b/types/ini.go @@ -102,7 +102,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 +111,7 @@ func (obj *Ini) AddKey(section, key, value string) error { return err } } - sect.AddKey(key, value) + sect.AddKey(key, value, reverse) return nil } diff --git a/types/section.go b/types/section.go index b67c337..932b45e 100644 --- a/types/section.go +++ b/types/section.go @@ -111,39 +111,54 @@ 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 - // replace first emptyline - for i := len(obj.Lines) - 1; i >= 0; i-- { - if obj.Lines[i].Type() == TEmptyLine { - replaceIndex = i - } else { - break + 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 + } } - } - // 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) + obj.Lines = append([]Element{&newKeyValue}, obj.Lines...) } else { - obj.Lines = append(obj.Lines, obj.Lines[replaceIndex]) - obj.Lines[replaceIndex] = &newKeyValue + // 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) { +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) } diff --git a/writer.go b/writer.go index e214bde..f0d10d3 100644 --- a/writer.go +++ b/writer.go @@ -4,15 +4,44 @@ import ( "bufio" "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) + 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) + + err = os.Remove(realOldFile) + if err != nil { + return err + } + + err = os.Rename(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 +62,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, mode) if err == nil { datawriter := bufio.NewWriter(targetFile) _, err = datawriter.WriteString(ini.Full())