27 Commits
0.1 ... 0.4.2

Author SHA1 Message Date
a703dd375c fix docs and readme 2020-12-17 21:18:14 +03:00
2fbd9030eb Update README.md
Add CodeQL badge
2020-12-17 21:07:31 +03:00
75265509f3 Create codeql-analysis.yml 2020-12-17 20:59:36 +03:00
ab44e1c12d Update README.md 2020-12-11 16:16:14 +03:00
8d912597cf feat: add auto test (github actions) 2020-12-11 16:14:01 +03:00
60c08d2a2d Merge branch 'master' of https://github.com/GenZmeY/multini 2020-12-11 16:04:34 +03:00
67648811a5 fix: save the version when building manually 2020-12-11 15:57:24 +03:00
9cc9af1759 perf: remove debuginfo from release binary 2020-12-11 15:38:12 +03:00
96aa1540ab Update README.md 2020-11-09 21:29:47 +03:00
88d1ec4201 Update README.md 2020-11-09 21:07:26 +03:00
47b9dba690 Update README.MD 2020-11-09 20:35:21 +03:00
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
72 changed files with 730 additions and 194 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
- 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: multini ${{ 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)

34
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '29 4 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

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

@ -0,0 +1,23 @@
name: tests
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) && make test

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 := $(shell git describe)
GOCMD=go GOCMD = go
LDFLAGS:="$(LDFLAGS) -X 'main.Version=$(VERSION)'" LDFLAGS := "$(LDFLAGS) -s -w -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)

109
README.md Normal file
View File

@ -0,0 +1,109 @@
# Multini
[![build](https://github.com/GenZmeY/multini/workflows/build/badge.svg)](https://github.com/GenZmeY/multini/actions?query=workflow%3Abuild)
[![tests](https://github.com/GenZmeY/multini/workflows/tests/badge.svg)](https://github.com/GenZmeY/multini/actions?query=workflow%3Atests)
[![CodeQL](https://github.com/GenZmeY/multini/workflows/CodeQL/badge.svg)](https://github.com/GenZmeY/multini/security/code-scanning)
[![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/GenZmeY/multini)](https://golang.org)
[![GitHub](https://img.shields.io/github/license/genzmey/multini)](LICENSE)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/GenZmeY/multini)](https://github.com/GenZmeY/multini/releases)
*Command line utility for manipulating ini files with duplicate key names.*
A compiled version of multini is available on the [release page](https://github.com/GenZmeY/multini/releases).
***
# Description
Some programs use ini file format with duplicate key names.
For example, these are games based on the [unreal engine](https://en.wikipedia.org/wiki/Unreal_Engine).
It might look like this (part of the Killing Floor 2 config):
```
[OnlineSubsystemSteamworks.KFWorkshopSteamworks]
ServerSubscribedWorkshopItems=2267561023
ServerSubscribedWorkshopItems=2085786712
ServerSubscribedWorkshopItems=2222630586
ServerSubscribedWorkshopItems=2146677560
```
Most implementations only support having one property with a given name in a section. If there are several of them, only the first (or last) key will be processed, which is not enough in this case. multini solves this problem.
**note:**
- multini is case sensitive;
- quotes around the value are not processed (they are part of the value for multini);
- multi-line values are not supported.
(but this may change in the future)
# Build & Install (Manual)
1. Install [golang](https://golang.org), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/);
2. Clone this repo: `git clone https://github.com/GenZmeY/multini`
3. Go to the source directory: `cd multini`
4. Build: `make`
5. Install: `make install`
# Usage
```
Usage: multini [OPTION]... ACTION ini_file [section] [key] [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.
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
```
# Examples
**output a global value not in a section:**
`multini --get ini_file '' key`
**output section:**
`multini --get ini_file section`
**output list of existing sections:**
`multini --get ini_file`
**output value:**
`multini --get ini_file section key`
- if there are several keys, a list of all values of this keys will be displayed
**create/update a single key:**
`multini --set ini_file section key value`
- if there is no key, it will be added
- if the key exists, the value will be updated
- if the key exists and has several values, the key with the specified value will be set, the rest of the values will be deleted
**add a key with specified value:**
`multini --add ini_file section key value`
- if there is no key, it will be added
- if the key exists and does not have the specified value, the new value will be added
- if the specified value repeats the existing one, no changes will be made
**delete all keys with specified name:**
`multini --del ini_file section key`
**delete a key with specified name and value:**
`multini --del ini_file section key value`
**delete a section:**
`multini --del ini_file section`
**short options can be combined:**
`multini -gq ini_file section key value`
- check the existence of a key with a given value using the return code
# License
Copyright © 2020 GenZmeY
The content of this repository is licensed under [MIT License](LICENSE).

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
@ -38,23 +40,24 @@ var (
func printHelp() { func printHelp() {
output.Println("A utility for manipulating ini files with duplicate keys") output.Println("A utility for manipulating ini files with duplicate keys")
output.Println("") output.Println("")
output.Println("Usage: multini [OPTION]... [ACTION] config_file [section] [param] [value]") output.Println("Usage: multini [OPTION]... [ACTION] config_file [section] [key] [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.")
output.Println(" -o, --output FILE Write output to FILE instead. '-' means stdout") 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(" -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 +77,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 +99,7 @@ func init() {
} }
func parseArgs() error { func parseArgs() error {
gnuflag.Parse(true) gnuflag.Parse(false)
// info // info
switch { switch {
@ -117,6 +124,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

@ -1,22 +1,22 @@
A utility for manipulating ini files with duplicate keys 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] [key] [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
}