change project structure

This commit is contained in:
2020-11-03 10:47:19 +03:00
parent c76140ffe1
commit 44e955a047
59 changed files with 0 additions and 0 deletions

68
cmd/multini/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, ArgReverse)
} 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)
}
}

186
cmd/multini/args.go Normal file
View File

@ -0,0 +1,186 @@
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
ArgReverse bool
ArgQuiet 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(" -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.")
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(" -q, --quiet Suppress all normal output")
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(&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", "", "")
gnuflag.StringVar(&ArgOutput, "o", "", "")
gnuflag.BoolVar(&ArgVersion, "version", false, "")
gnuflag.BoolVar(&ArgHelp, "help", false, "")
gnuflag.BoolVar(&ArgHelp, "h", false, "")
}
func parseArgs() error {
gnuflag.Parse(false)
// 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)
output.SetQuiet(ArgQuiet)
// 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")
}
}

79
cmd/multini/main.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"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 {
output.Errorln(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)
}

182
cmd/multini/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
}

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