8 Commits
0.3.0 ... 0.4.1

Author SHA1 Message Date
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
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
10 changed files with 231 additions and 61 deletions

View File

@ -23,7 +23,7 @@ jobs:
go-version: '1.13.0'
- name: Build
run: make -j $(nproc) compile VERSION=${{ steps.get_version.outputs.VERSION }}
run: make -j $(nproc) compile
- name: create release
id: create_release
@ -32,7 +32,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_version.outputs.VERSION }}
release_name: Release ${{ steps.get_version.outputs.VERSION }}
release_name: multini ${{ steps.get_version.outputs.VERSION }}
draft: false
prerelease: false

23
.gitignore vendored
View File

@ -1,20 +1,3 @@
# Binaries for programs and plugins
bin/*
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/
/bin/
/test/data/out_ini/
/cmd/multini/multini

View File

@ -1,7 +1,7 @@
NAME = multini
VERSION = dev_$(shell date +%F_%T)
VERSION := $(shell git describe)
GOCMD = go
LDFLAGS := "$(LDFLAGS) -X 'main.Version=$(VERSION)'"
LDFLAGS := "$(LDFLAGS) -s -w -X 'main.Version=$(VERSION)'"
GOBUILD = $(GOCMD) build -ldflags=$(LDFLAGS)
SRCMAIN = ./cmd/$(NAME)
SRCDOC = ./doc

107
README.md Normal file
View File

@ -0,0 +1,107 @@
# Multini
[![build](https://github.com/GenZmeY/multini/workflows/build/badge.svg)](https://github.com/GenZmeY/multini/actions)
[![GitHub top language](https://img.shields.io/github/languages/top/GenZmeY/multini)](https://golang.org)
[![GitHub](https://img.shields.io/github/license/genzmey/multini)](https://github.com/GenZmeY/multini/blob/master/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] [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.
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 '' param`
**output section:**
`multini --get ini_file section`
**output list of existing sections:**
`multini --get ini_file`
**output value:**
`multini --get ini_file section param`
- if there are several parameters, a list of all values of this parameter will be displayed
**add/update a single parameter:**
`multini --set ini_file section parameter value`
- if there is no parameter, it will be added
- if the parameter exists, the value will be updated
- if the parameter exists and has several values, the parameter with the specified value will be set, the rest of the values will be deleted
**add a parameter with specified value:**
`multini --add ini_file section parameter value`
- if there is no parameter, it will be added
- if the parameter 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 parameters with specified name:**
`multini --del ini_file section parameter`
**delete a parameter with specified name and value:**
`multini --del ini_file section parameter value`
**delete a section:**
`multini --del ini_file section`
**short options can be combined:**
`multini -gq ini_file section parameter value`
- check the existence of a parameter with a given value using the return code
# License
Copyright © 2020 GenZmeY
The content of this repository is licensed under [MIT License](https://github.com/GenZmeY/multini/blob/master/LICENSE).

View File

@ -22,34 +22,88 @@ var (
NgValuePrefix string = `value_prefix`
NgValuePostfix string = `value_postfix`
NgComment string = `comment`
NgCommentPrefix string = `comment_prefix`
NgData string = `data`
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)
RxEmpty string = `^(?P<` + NgPrefix + `>\s+)?$`
RxSection string = `^(?P<` + NgPrefix + `>\s+)?\[(?P<` + NgSection + `>[^\]]+)\](?P<` + NgPostifx + `>\s+)?$`
RxKey string = `^(?P<` + NgPrefix + `>\s+)?(?P<` + NgKey + `>.*[^\s]+)(?P<` + NgKeyPostfix + `>\s+)?$`
RxValue string = `^(?P<` + NgValuePrefix + `>\s+)?(?P<` + NgValue + `>.*[^\s])(?P<` + NgValuePostfix + `>\s+)?$`
RxEmptyCompile *regexp.Regexp = regexp.MustCompile(RxEmpty)
RxSectionCompile *regexp.Regexp = regexp.MustCompile(RxSection)
RxKeyCompile *regexp.Regexp = regexp.MustCompile(RxKey)
RxValueCompile *regexp.Regexp = regexp.MustCompile(RxValue)
)
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]
}
func parse(str string) map[string]string {
var result map[string]string = make(map[string]string)
var data string
data, result[NgComment] = getDataComment(str)
if data != "" {
findNamedGroups(data, RxEmptyCompile, &result)
}
if result[NgPrefix] != "" {
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 {
@ -61,14 +115,14 @@ func debugMap(el map[string]string) string {
}
func appendLine(ini *types.Ini, line string) error {
elements := rxParse(RxCompiled, line)
// elements := rxParse(line)
elements := parse(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)
@ -87,7 +141,6 @@ func appendLine(ini *types.Ini, line string) error {
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
@ -98,8 +151,8 @@ func appendLine(ini *types.Ini, line string) error {
}
case elements[NgComment] != "":
var newComment types.Comment
newComment.Prefix = elements[NgPrefix] + elements[NgCommentPrefix]
newComment.Value = elements[NgComment]
newComment.Prefix = elements[NgPrefix]
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

View File

@ -1,6 +1,6 @@
# comment
; comment again
// comment with indent
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!

View File

@ -0,0 +1 @@
skip/skip

View File

@ -1,6 +1,6 @@
# comment
; comment again
// comment with indent
DefKey1 = Some Value1
DefKey2 = Some Value2 And Tabs! # With Comment
DefKey3=NoSpaces!

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

@ -0,0 +1,7 @@
#!/bin/bash
source "common.sh"
$Multini --get "$InIni" 'Slashes/Test' './Dir1/File' > "$OutIni"
compare