diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c679d06..16982ff 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,9 +4,7 @@ _Before opening an issue, search for similar bug reports or feature requests on **System info:** -* **Version used (`yo -v`):** -* **OS (e.g. from `/etc/*-release`):** -* **Kernel (`uname -a`):** +* **Verbose version info (`yo -vv`):** * **Install tools:** **Steps to reproduce:** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732262b..435bcf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,18 @@ on: branches: [master] schedule: - cron: '0 12 */15 * *' + workflow_dispatch: + inputs: + force_run: + description: 'Force workflow run' + required: true + type: choice + options: [yes, no] + +permissions: + actions: read + contents: read + statuses: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -22,11 +34,11 @@ jobs: strategy: matrix: - go: [ '1.18.x', '1.19.x' ] + go: [ '1.19.x', '1.20.x' ] steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} @@ -55,9 +67,9 @@ jobs: steps: - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: '1.17.x' + go-version: '1.19.x' - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml index 3e52f56..45e9898 100644 --- a/.github/workflows/docker-push.yml +++ b/.github/workflows/docker-push.yml @@ -135,7 +135,7 @@ jobs: - name: Build and push Docker images (Docker) if: ${{ steps.build_check.outputs.build == 'true' }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: push: true context: . @@ -148,7 +148,7 @@ jobs: - name: Build and push Docker images (GHCR) if: ${{ steps.build_check.outputs.build == 'true' }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: push: true context: . diff --git a/Makefile b/Makefile index 54d8834..7cd3b00 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ################################################################################ -# This Makefile generated by GoMakeGen 2.1.0 using next command: +# This Makefile generated by GoMakeGen 2.2.0 using next command: # gomakegen --mod . # # More info: https://kaos.sh/gomakegen @@ -94,6 +94,6 @@ help: ## Show this info | sed 's/ifdef //' \ | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-14s\033[0m %s\n", $$1, $$2}' @echo -e '' - @echo -e '\033[90mGenerated by GoMakeGen 2.1.0\033[0m\n' + @echo -e '\033[90mGenerated by GoMakeGen 2.2.0\033[0m\n' ################################################################################ diff --git a/README.md b/README.md index 5cf34f3..0b5077d 100644 --- a/README.md +++ b/README.md @@ -18,22 +18,16 @@ Yo is a command-line YAML processor. #### From source -To build the Yo from scratch, make sure you have a working Go 1.17+ workspace (_[instructions](https://golang.org/doc/install)_), then: +To build the Yo from scratch, make sure you have a working Go 1.19+ workspace (_[instructions](https://golang.org/doc/install)_), then: ``` -go get github.com/essentialkaos/yo -``` - -If you want update Yo to latest stable release, do: - -``` -go get -u github.com/essentialkaos/yo +go install github.com/essentialkaos/yo@latest ``` #### From [ESSENTIAL KAOS Public Repository](https://yum.kaos.st) ```bash -sudo yum install -y https://yum.kaos.st/get/$(uname -r).rpm +sudo yum install -y https://yum.kaos.st/kaos-repo-latest.el$(grep 'CPE_NAME' /etc/os-release | tr -d '"' | cut -d':' -f5).noarch.rpm sudo yum install yo ``` diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..2100631 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,630 @@ +package cli + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2023 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "io/ioutil" + "os" + "sort" + "strconv" + "strings" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fsutil" + "github.com/essentialkaos/ek/v12/options" + "github.com/essentialkaos/ek/v12/usage" + "github.com/essentialkaos/ek/v12/usage/completion/bash" + "github.com/essentialkaos/ek/v12/usage/completion/fish" + "github.com/essentialkaos/ek/v12/usage/completion/zsh" + "github.com/essentialkaos/ek/v12/usage/man" + "github.com/essentialkaos/ek/v12/usage/update" + + "github.com/essentialkaos/go-simpleyaml/v2" + + "github.com/essentialkaos/yo/cli/support" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +const ( + APP = "Yo" + VER = "0.5.6" + DESC = "Command-line YAML processor" +) + +const ( + OPT_FROM_FILE = "f:from-file" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "h:help" + OPT_VER = "v:version" + + OPT_VERB_VER = "vv:verbose-version" + OPT_COMPLETION = "completion" + OPT_GENERATE_MAN = "generate-man" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +type Query struct { + Tokens []Token + Processors []string +} + +type Token struct { + Key string + Index []int + Range Range +} + +type Range struct { + Start int + End int +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var optMap = options.Map{ + OPT_FROM_FILE: {Type: options.STRING}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL}, + OPT_VER: {Type: options.BOOL}, + + OPT_VERB_VER: {Type: options.BOOL}, + OPT_COMPLETION: {}, + OPT_GENERATE_MAN: {Type: options.BOOL}, +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Init is main application function +func Run(gitRev string, gomod []byte) { + preConfigureUI() + + args, errs := options.Parse(optMap) + + if len(errs) != 0 { + printError(errs[0].Error()) + os.Exit(1) + } + + configureUI() + + switch { + case options.Has(OPT_COMPLETION): + os.Exit(printCompletion()) + case options.Has(OPT_GENERATE_MAN): + printMan() + os.Exit(0) + case options.GetB(OPT_VER): + genAbout(gitRev).Print() + os.Exit(0) + case options.GetB(OPT_VERB_VER): + support.Print(APP, VER, gitRev, gomod) + os.Exit(0) + case options.GetB(OPT_HELP), + len(args) == 0 && !options.Has(OPT_FROM_FILE): + genUsage().Print() + os.Exit(0) + } + + process(strings.Join(args.Strings(), " ")) +} + +// preConfigureUI preconfigures UI based on information about user terminal +func preConfigureUI() { + term := os.Getenv("TERM") + + fmtc.DisableColors = true + + if term != "" { + switch { + case strings.Contains(term, "xterm"), + strings.Contains(term, "color"), + term == "screen": + fmtc.DisableColors = false + } + } + + if !fsutil.IsCharacterDevice("/dev/stdout") && os.Getenv("FAKETTY") == "" { + fmtc.DisableColors = true + } + + if os.Getenv("NO_COLOR") != "" { + fmtc.DisableColors = true + } +} + +// configureUI configures user interface +func configureUI() { + if options.GetB(OPT_NO_COLOR) { + fmtc.DisableColors = true + } +} + +// readData reads data from standart input or file +func readData() ([]byte, error) { + if options.Has(OPT_FROM_FILE) { + return readFromFile(options.GetS(OPT_FROM_FILE)) + } + + return readFromStdin() +} + +// readFromFile reads data from file +func readFromFile(file string) ([]byte, error) { + return ioutil.ReadFile(file) +} + +// readFromStdin reads data from standart input +func readFromStdin() ([]byte, error) { + return ioutil.ReadFile("/dev/stdin") +} + +// process start data processing +func process(query string) { + data, err := readData() + + if err != nil { + printError(err.Error()) + os.Exit(1) + } + + yaml, err := simpleyaml.NewYaml(data) + + if err != nil { + printError(err.Error()) + os.Exit(1) + } + + execQuery(yaml, query) +} + +// execQuery executes query over YAML +func execQuery(yaml *simpleyaml.Yaml, query string) { + var data []*simpleyaml.Yaml + + for _, q := range parseQuery(query) { + data = []*simpleyaml.Yaml{yaml} + + for _, t := range q.Tokens { + if len(data) == 0 { + break + } + + if t.IsArrayToken() || data[0].IsArray() || data[0].Get(t.Key).IsArray() { + data = execArrayTokenSelector(t, data) + } else { + data = execBasicTokenSelector(t, data) + } + } + + if len(q.Processors) == 0 { + renderData(data) + } else { + processData(q.Processors, data) + } + } +} + +// execArrayTokenSelector executes array query token over given data +func execArrayTokenSelector(t Token, data []*simpleyaml.Yaml) []*simpleyaml.Yaml { + var result []*simpleyaml.Yaml + + if len(t.Index) != 0 { + for _, item := range data { + for _, index := range t.Index { + if t.Key == "" { + if item.IsIndexExist(index) { + result = append(result, item.GetByIndex(index)) + } + } else { + if item.Get(t.Key).IsIndexExist(index) { + result = append(result, item.Get(t.Key).GetByIndex(index)) + } + } + } + } + } else { + for _, item := range data { + if t.Range.Start == -1 && t.Range.End == -1 { + if item.IsExist(t.Key) { + result = append(result, item.Get(t.Key)) + } + + continue + } + + RANGELOOP: + for index := t.Range.Start; index < t.Range.End; index++ { + if t.Key == "" { + if item.IsIndexExist(index) { + result = append(result, item.GetByIndex(index)) + } else { + break RANGELOOP + } + } else { + if item.Get(t.Key).IsIndexExist(index) { + result = append(result, item.Get(t.Key).GetByIndex(index)) + } else { + break RANGELOOP + } + } + } + } + } + + return result +} + +// execBasicTokenSelector executes basic query token over given data +func execBasicTokenSelector(t Token, data []*simpleyaml.Yaml) []*simpleyaml.Yaml { + var result []*simpleyaml.Yaml + + for _, item := range data { + if item.IsExist(t.Key) { + result = append(result, item.Get(t.Key)) + } + } + + return result +} + +// renderData renders yaml structs to string +func renderData(data []*simpleyaml.Yaml) { + for _, item := range data { + switch { + case item.IsArray(): + if item.GetByIndex(0).IsMap() || item.GetByIndex(0).IsArray() { + encodeYaml(item) + } else { + fmt.Println(strings.Join(item.MustStringArray(nil), "\n")) + } + + case item.IsMap(): + encodeYaml(item) + + default: + fmt.Printf("%v\n", item.Interface()) + } + } +} + +// processData runs processors over given data +func processData(processor []string, data []*simpleyaml.Yaml) { + var result interface{} + + for _, pf := range processor { + switch pf { + case "len", "length": + result = processorFuncLength(data, result) + case "keys": + result = processorFuncKeys(data, result) + case "sort": + result = processorFuncSort(result) + default: + printError("Unknown function \"%s\"", pf) + os.Exit(1) + } + } + + switch result.(type) { + case string, int: + fmt.Println(result) + case []int: + for _, v := range result.([]int) { + fmt.Println(v) + } + case []string: + for _, v := range result.([]string) { + fmt.Println(v) + } + } +} + +// processorFuncLength is a length processor +func processorFuncLength(data []*simpleyaml.Yaml, k interface{}) []int { + var result []int + + if k == nil { + for _, item := range data { + switch { + case item.IsArray(): + result = append(result, len(item.MustArray(nil))) + case item.IsMap(): + result = append(result, len(item.MustMap(nil))) + default: + result = append(result, len(item.MustString(""))) + } + } + } else { + switch k.(type) { + case string: + return []int{len(k.(string))} + case []string: + return []int{len(k.([]string))} + } + } + + return result +} + +// processorFuncKeys is a keys processor +func processorFuncKeys(data []*simpleyaml.Yaml, k interface{}) []string { + var result []string + + if k != nil { + return nil + } + + for _, item := range data { + if item.IsMap() { + keys, _ := item.GetMapKeys() + result = append(result, keys...) + } + } + + return result +} + +// processorFuncKeys is a sort processor +func processorFuncSort(k interface{}) []string { + var result []string + + switch k.(type) { + case string: + result = []string{k.(string)} + case []string: + result = k.([]string) + sort.Strings(result) + } + + return result +} + +// parseQuery parses query +func parseQuery(query string) []Query { + var result []Query + + for _, q := range splitQuery(query) { + result = append(result, parseSubQuery(q)) + } + + return result +} + +// parseSubQuery parses sub-query +func parseSubQuery(query string) Query { + query = strings.TrimSpace(query) + + if !strings.Contains(query, "|") { + return Query{Tokens: parseTokens(query)} + } + + qs := strings.Split(query, "|") + + if len(qs) < 2 { + return Query{Tokens: parseTokens(qs[0])} + } + + return Query{Tokens: parseTokens(qs[0]), Processors: parseProcessors(qs[1:])} +} + +// parseTokens splits query to tokens +func parseTokens(query string) []Token { + query = strings.TrimSpace(query) + + var result []Token + + for i, t := range strings.Split(query, ".") { + if i == 0 || t == "" { + continue + } + + result = append(result, parseToken(t)) + } + + return result +} + +// parseToken parses token +func parseToken(token string) Token { + if strings.Contains(token, "[") && strings.Contains(token, "]") { + is := strings.Index(token, "[") + return parseArrayToken(token[:is], token[is:]) + } + + return Token{Key: token, Range: Range{-1, -1}} +} + +// parseArrayToken parses array token +func parseArrayToken(key, index string) Token { + if index == "[]" { + return Token{Key: key, Range: Range{0, 999999999}} + } + + index = strings.TrimLeft(index, "[") + index = strings.TrimRight(index, "]") + + if strings.Contains(index, ":") { + is := strings.Split(index, ":") + + return Token{ + Key: key, + Range: Range{ + str2int(is[0], 0), + str2int(is[1], 999999999), + }, + } + } else if strings.Contains(index, ",") { + return Token{Key: key, Range: Range{-1, -1}, Index: converEnum(strings.Split(index, ","))} + } else { + return Token{Key: key, Range: Range{-1, -1}, Index: []int{str2int(index, 0)}} + } +} + +// parseProcessors parses processors +func parseProcessors(processors []string) []string { + var result []string + + for _, p := range processors { + result = append(result, strings.TrimSpace(p)) + } + + return result +} + +// splitQuery splits query +func splitQuery(query string) []string { + var result []string + var buffer string + var isArray bool + + for _, r := range query { + switch r { + case '[': + isArray = true + case ']': + isArray = false + } + + if r == ',' && !isArray { + result = append(result, buffer) + buffer = "" + continue + } + + buffer += string(r) + } + + if buffer != "" { + result = append(result, buffer) + } + + return result +} + +// converEnum converts string slice to int slice +func converEnum(s []string) []int { + var result []int + + for _, i := range s { + result = append(result, str2int(i, 0)) + } + + return result +} + +// str2int converts string to int +func str2int(s string, def int) int { + s = strings.TrimSpace(s) + + if s == "" { + return def + } + + i, _ := strconv.Atoi(s) + + return i +} + +// encodeYaml encodes yaml struct to string +func encodeYaml(yaml *simpleyaml.Yaml) { + data, _ := yaml.MarshalYAML() + + // Print encoded YAML without new line symbol + fmt.Println(string(data[:len(data)-1])) +} + +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// IsArrayToken returns true if it array selector token +func (t Token) IsArrayToken() bool { + if len(t.Index) != 0 || t.Range.Start != -1 || t.Range.End != -1 { + return true + } + + return false +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// printCompletion prints completion for given shell +func printCompletion() int { + info := genUsage() + + switch options.GetS(OPT_COMPLETION) { + case "bash": + fmt.Printf(bash.Generate(info, "yo")) + case "fish": + fmt.Printf(fish.Generate(info, "yo")) + case "zsh": + fmt.Printf(zsh.Generate(info, optMap, "yo")) + default: + return 1 + } + + return 0 +} + +// printMan prints man page +func printMan() { + fmt.Println( + man.Generate( + genUsage(), + genAbout(""), + ), + ) +} + +// genUsage generates usage info +func genUsage() *usage.Info { + info := usage.NewInfo("", "query") + + info.AddOption(OPT_FROM_FILE, "Read data from file", "filename") + info.AddOption(OPT_NO_COLOR, "Disable colors in output") + info.AddOption(OPT_HELP, "Show this help message") + info.AddOption(OPT_VER, "Show version") + + info.AddRawExample("cat file.yml | yo '.foo'", "Return value for key foo") + info.AddExample("-f file.yml '.foo'", "Return value for key foo") + info.AddExample("-f file.yml '.foo | length'", "Print value length") + info.AddExample("-f file.yml '.foo[]'", "Return all items from array") + info.AddExample("-f file.yml '.bar[2:]'", "Return subarray started from item with index 2") + info.AddExample("-f file.yml '.bar[1,2,5]'", "Return items with index 1, 2 and 5 from array") + info.AddExample("-f file.yml '.bar[] | length'", "Print array size") + info.AddExample("-f file.yml '.xyz | keys'", "Print hash map keys") + info.AddExample("-f file.yml '.xyz | keys | length'", "Print number of hash map keys") + info.AddExample("-f file.yml '.xyz | keys | sort'", "Print sorted list of keys") + + return info +} + +// genAbout generates info about version +func genAbout(gitRev string) *usage.About { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2006, + Owner: "ESSENTIAL KAOS", + License: "Apache License, Version 2.0 ", + UpdateChecker: usage.UpdateChecker{"essentialkaos/yo", update.GitHubChecker}, + } + + if gitRev != "" { + about.Build = "git:" + gitRev + } + + return about +} diff --git a/cli/support/support.go b/cli/support/support.go new file mode 100644 index 0000000..4661ad2 --- /dev/null +++ b/cli/support/support.go @@ -0,0 +1,168 @@ +package support + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2023 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "runtime" + "strings" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/hash" + "github.com/essentialkaos/ek/v12/strutil" + "github.com/essentialkaos/ek/v12/system" + "github.com/essentialkaos/ek/v12/system/container" + + "github.com/essentialkaos/depsy" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Print prints verbose info about application, system, dependencies and +// important environment +func Print(app, ver, gitRev string, gomod []byte) { + fmtutil.SeparatorTitleColorTag = "{s-}" + fmtutil.SeparatorFullscreen = false + fmtutil.SeparatorColorTag = "{s-}" + fmtutil.SeparatorSize = 80 + + showApplicationInfo(app, ver, gitRev) + showOSInfo() + showDepsInfo(gomod) + + fmtutil.Separator(false) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showApplicationInfo shows verbose information about application +func showApplicationInfo(app, ver, gitRev string) { + fmtutil.Separator(false, "APPLICATION INFO") + + printInfo(7, "Name", app) + printInfo(7, "Version", ver) + + printInfo(7, "Go", fmtc.Sprintf( + "%s {s}(%s/%s){!}", + strings.TrimLeft(runtime.Version(), "go"), + runtime.GOOS, runtime.GOARCH, + )) + + if gitRev != "" { + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + printInfo(7, "Git SHA", gitRev+getHashColorBullet(gitRev)) + } else { + printInfo(7, "Git SHA", gitRev) + } + } + + bin, _ := os.Executable() + binSHA := hash.FileHash(bin) + + if binSHA != "" { + binSHA = strutil.Head(binSHA, 7) + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + printInfo(7, "Bin SHA", binSHA+getHashColorBullet(binSHA)) + } else { + printInfo(7, "Bin SHA", binSHA) + } + } +} + +// showOSInfo shows verbose information about system +func showOSInfo() { + osInfo, err := system.GetOSInfo() + + if err == nil { + fmtutil.Separator(false, "OS INFO") + + printInfo(12, "Name", osInfo.Name) + printInfo(12, "Pretty Name", osInfo.PrettyName) + printInfo(12, "Version", osInfo.VersionID) + printInfo(12, "ID", osInfo.ID) + printInfo(12, "ID Like", osInfo.IDLike) + printInfo(12, "Version ID", osInfo.VersionID) + printInfo(12, "Version Code", osInfo.VersionCodename) + printInfo(12, "CPE", osInfo.CPEName) + } + + systemInfo, err := system.GetSystemInfo() + + if err != nil { + return + } else { + if osInfo == nil { + fmtutil.Separator(false, "SYSTEM INFO") + printInfo(12, "Name", systemInfo.OS) + } + } + + printInfo(12, "Arch", systemInfo.Arch) + printInfo(12, "Kernel", systemInfo.Kernel) + + containerEngine := "No" + + switch container.GetEngine() { + case container.DOCKER: + containerEngine = "Yes (Docker)" + case container.PODMAN: + containerEngine = "Yes (Podman)" + case container.LXC: + containerEngine = "Yes (LXC)" + } + + fmtc.NewLine() + + printInfo(12, "Container", containerEngine) +} + +// showDepsInfo shows information about all dependencies +func showDepsInfo(gomod []byte) { + deps := depsy.Extract(gomod, true) + + if len(deps) == 0 { + return + } + + fmtutil.Separator(false, "DEPENDENCIES") + + for _, dep := range deps { + if dep.Extra == "" { + fmtc.Printf(" {s}%8s{!} %s\n", dep.Version, dep.Path) + } else { + fmtc.Printf(" {s}%8s{!} %s {s-}(%s){!}\n", dep.Version, dep.Path, dep.Extra) + } + } +} + +// getHashColorBullet return bullet with color from hash +func getHashColorBullet(v string) string { + if len(v) > 6 { + v = strutil.Head(v, 6) + } + + return fmtc.Sprintf(" {#" + strutil.Head(v, 6) + "}● {!}") +} + +// printInfo formats and prints info record +func printInfo(size int, name, value string) { + name = name + ":" + size++ + + if value == "" { + fm := fmt.Sprintf(" {*}%%-%ds{!} {s-}—{!}\n", size) + fmtc.Printf(fm, name) + } else { + fm := fmt.Sprintf(" {*}%%-%ds{!} %%s\n", size) + fmtc.Printf(fm, name, value) + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // diff --git a/common/yo.spec b/common/yo.spec index e41af32..cd11931 100644 --- a/common/yo.spec +++ b/common/yo.spec @@ -1,62 +1,24 @@ ################################################################################ -# rpmbuilder:relative-pack true +%define debug_package %{nil} ################################################################################ -%define debug_package %{nil} +Summary: Command-line YAML processor +Name: yo +Version: 0.5.6 +Release: 0%{?dist} +Group: Applications/System +License: Apache License, Version 2.0 +URL: https://kaos.sh/yo -################################################################################ - -%define _posixroot / -%define _root /root -%define _bin /bin -%define _sbin /sbin -%define _srv /srv -%define _home /home -%define _opt /opt -%define _lib32 %{_posixroot}lib -%define _lib64 %{_posixroot}lib64 -%define _libdir32 %{_prefix}%{_lib32} -%define _libdir64 %{_prefix}%{_lib64} -%define _logdir %{_localstatedir}/log -%define _rundir %{_localstatedir}/run -%define _lockdir %{_localstatedir}/lock/subsys -%define _cachedir %{_localstatedir}/cache -%define _spooldir %{_localstatedir}/spool -%define _crondir %{_sysconfdir}/cron.d -%define _loc_prefix %{_prefix}/local -%define _loc_exec_prefix %{_loc_prefix} -%define _loc_bindir %{_loc_exec_prefix}/bin -%define _loc_libdir %{_loc_exec_prefix}/%{_lib} -%define _loc_libdir32 %{_loc_exec_prefix}/%{_lib32} -%define _loc_libdir64 %{_loc_exec_prefix}/%{_lib64} -%define _loc_libexecdir %{_loc_exec_prefix}/libexec -%define _loc_sbindir %{_loc_exec_prefix}/sbin -%define _loc_bindir %{_loc_exec_prefix}/bin -%define _loc_datarootdir %{_loc_prefix}/share -%define _loc_includedir %{_loc_prefix}/include -%define _loc_mandir %{_loc_datarootdir}/man -%define _rpmstatedir %{_sharedstatedir}/rpm-state -%define _pkgconfigdir %{_libdir}/pkgconfig - -################################################################################ - -Summary: Command-line YAML processor -Name: yo -Version: 0.5.5 -Release: 0%{?dist} -Group: Applications/System -License: Apache License, Version 2.0 -URL: https://kaos.sh/yo +Source0: https://source.kaos.st/%{name}/%{name}-%{version}.tar.bz2 -Source0: https://source.kaos.st/%{name}/%{name}-%{version}.tar.bz2 +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) -BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) +BuildRequires: golang >= 1.19 -BuildRequires: golang >= 1.19 - -Provides: %{name} = %{version}-%{release} +Provides: %{name} = %{version}-%{release} ################################################################################ @@ -69,6 +31,11 @@ Command-line YAML processor. %setup -q %build +if [[ ! -d "%{name}/vendor" ]] ; then + echo "This package requires vendored dependencies" + exit 1 +fi + pushd %{name} go build %{name}.go cp LICENSE .. @@ -126,6 +93,11 @@ fi ################################################################################ %changelog +* Mon Mar 06 2023 Anton Novojilov - 0.5.6-0 +- Added verbose info output +- Dependencies update +- Code refactoring + * Wed Nov 23 2022 Anton Novojilov - 0.5.5-0 - Fixed build using sources from source.kaos.st - Dependencies update diff --git a/go.mod b/go.mod index 79b10f7..2ca9a38 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/essentialkaos/yo go 1.19 require ( - github.com/essentialkaos/ek/v12 v12.57.0 - github.com/essentialkaos/go-simpleyaml/v2 v2.1.3 + github.com/essentialkaos/depsy v1.0.0 + github.com/essentialkaos/ek/v12 v12.63.0 + github.com/essentialkaos/go-simpleyaml/v2 v2.1.4 ) require ( - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect + golang.org/x/sys v0.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index f068650..009e137 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,15 @@ -github.com/essentialkaos/check v1.3.0 h1:ria+8o22RCLdt2D/1SHQsEH5Mmy5S+iWHaGHrrbPUc0= -github.com/essentialkaos/ek/v12 v12.57.0 h1:FUl5ZUF7czjZXROYVoJamGTWqEIGuZEHrm1DLfzq43I= -github.com/essentialkaos/ek/v12 v12.57.0/go.mod h1:G8ghiSKh8ToJQCdB2bAhE3CnI6dn9nTJdWH3bQIVr1U= -github.com/essentialkaos/go-simpleyaml/v2 v2.1.3 h1:DAFvXut4ZtkuiTKWqSux3W0myuv3TOAwiD6w5MKWxsM= -github.com/essentialkaos/go-simpleyaml/v2 v2.1.3/go.mod h1:LAUFukPmTZ4fmHa1K3LsH8MO257r/CL/BqlaP/2Vgcs= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/essentialkaos/check v1.4.0 h1:kWdFxu9odCxUqo1NNFNJmguGrDHgwi3A8daXX1nkuKk= +github.com/essentialkaos/depsy v1.0.0 h1:FikBtTnNhk+xFO/hFr+CfiKs6QnA3wMD6tGL0XTEUkc= +github.com/essentialkaos/depsy v1.0.0/go.mod h1:XVsB2eVUonEzmLKQP3ig2P6v2+WcHVgJ10zm0JLqFMM= +github.com/essentialkaos/ek/v12 v12.63.0 h1:9yaEu5W3bx//9y52ShqYCoFDKOcwEdrnvgSkUYyatgI= +github.com/essentialkaos/ek/v12 v12.63.0/go.mod h1:9MlSuHpewu7OZ9tM9dLFHvoA8dflBIUPCA0Ctt97wRs= +github.com/essentialkaos/go-simpleyaml/v2 v2.1.4 h1:B2bXdGWaQ6Xy3HTsO2DLBoNV7cbMW3KDmeeOKYLo9z0= +github.com/essentialkaos/go-simpleyaml/v2 v2.1.4/go.mod h1:pVQTleUBC8xBI9+HnTF38xWBZqANNrmMAzwmUemblJg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/yo.go b/yo.go index 4af7d27..491eb34 100644 --- a/yo.go +++ b/yo.go @@ -2,633 +2,27 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2022 ESSENTIAL KAOS // +// Copyright (c) 2023 ESSENTIAL KAOS // // Apache License, Version 2.0 // // // // ////////////////////////////////////////////////////////////////////////////////// // import ( - "fmt" - "io/ioutil" - "os" - "sort" - "strconv" - "strings" + _ "embed" - "github.com/essentialkaos/ek/v12/env" - "github.com/essentialkaos/ek/v12/fmtc" - "github.com/essentialkaos/ek/v12/fsutil" - "github.com/essentialkaos/ek/v12/options" - "github.com/essentialkaos/ek/v12/usage" - "github.com/essentialkaos/ek/v12/usage/completion/bash" - "github.com/essentialkaos/ek/v12/usage/completion/fish" - "github.com/essentialkaos/ek/v12/usage/completion/zsh" - "github.com/essentialkaos/ek/v12/usage/man" - "github.com/essentialkaos/ek/v12/usage/update" - - "github.com/essentialkaos/go-simpleyaml/v2" -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -const ( - APP = "Yo" - VER = "0.5.5" - DESC = "Command-line YAML processor" + CLI "github.com/essentialkaos/yo/cli" ) -const ( - OPT_FROM_FILE = "f:from-file" - OPT_NO_COLOR = "nc:no-color" - OPT_HELP = "h:help" - OPT_VER = "v:version" - - OPT_COMPLETION = "completion" - OPT_GENERATE_MAN = "generate-man" -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -type Query struct { - Tokens []Token - Processors []string -} - -type Token struct { - Key string - Index []int - Range Range -} - -type Range struct { - Start int - End int -} - // ////////////////////////////////////////////////////////////////////////////////// // -var optMap = options.Map{ - OPT_FROM_FILE: {Type: options.STRING}, - OPT_NO_COLOR: {Type: options.BOOL}, - OPT_HELP: {Type: options.BOOL}, - OPT_VER: {Type: options.BOOL}, +//go:embed go.mod +var gomod []byte - OPT_COMPLETION: {}, - OPT_GENERATE_MAN: {Type: options.BOOL}, -} +// gitrev is short hash of the latest git commit +var gitrev string // ////////////////////////////////////////////////////////////////////////////////// // func main() { - args, errs := options.Parse(optMap) - - if len(errs) != 0 { - for _, err := range errs { - printError(err.Error()) - } - - os.Exit(1) - } - - if options.Has(OPT_COMPLETION) { - os.Exit(genCompletion()) - } - - if options.Has(OPT_GENERATE_MAN) { - os.Exit(genMan()) - } - - configureUI() - - if options.GetB(OPT_VER) { - showAbout() - os.Exit(1) - } - - if options.GetB(OPT_HELP) { - showUsage() - os.Exit(1) - } - - if len(args) == 0 && !options.Has(OPT_FROM_FILE) { - showUsage() - os.Exit(1) - } - - process(strings.Join(args.Strings(), " ")) -} - -// configureUI configures user interface -func configureUI() { - envVars := env.Get() - term := envVars.GetS("TERM") - - fmtc.DisableColors = true - - if term != "" { - switch { - case strings.Contains(term, "xterm"), - strings.Contains(term, "color"), - term == "screen": - fmtc.DisableColors = false - } - } - - if options.GetB(OPT_NO_COLOR) { - fmtc.DisableColors = true - } - - if !fsutil.IsCharacterDevice("/dev/stdout") && envVars.GetS("FAKETTY") == "" { - fmtc.DisableColors = true - } -} - -// readData reads data from standart input or file -func readData() ([]byte, error) { - if options.Has(OPT_FROM_FILE) { - return readFromFile(options.GetS(OPT_FROM_FILE)) - } - - return readFromStdin() -} - -// readFromFile reads data from file -func readFromFile(file string) ([]byte, error) { - return ioutil.ReadFile(file) -} - -// readFromStdin reads data from standart input -func readFromStdin() ([]byte, error) { - return ioutil.ReadFile("/dev/stdin") -} - -// process start data processing -func process(query string) { - data, err := readData() - - if err != nil { - printError(err.Error()) - os.Exit(1) - } - - yaml, err := simpleyaml.NewYaml(data) - - if err != nil { - printError(err.Error()) - os.Exit(1) - } - - execQuery(yaml, query) -} - -// execQuery executes query over YAML -func execQuery(yaml *simpleyaml.Yaml, query string) { - var data []*simpleyaml.Yaml - - for _, q := range parseQuery(query) { - data = []*simpleyaml.Yaml{yaml} - - for _, t := range q.Tokens { - if len(data) == 0 { - break - } - - if t.IsArrayToken() || data[0].IsArray() || data[0].Get(t.Key).IsArray() { - data = execArrayTokenSelector(t, data) - } else { - data = execBasicTokenSelector(t, data) - } - } - - if len(q.Processors) == 0 { - renderData(data) - } else { - processData(q.Processors, data) - } - } -} - -// execArrayTokenSelector executes array query token over given data -func execArrayTokenSelector(t Token, data []*simpleyaml.Yaml) []*simpleyaml.Yaml { - var result []*simpleyaml.Yaml - - if len(t.Index) != 0 { - for _, item := range data { - for _, index := range t.Index { - if t.Key == "" { - if item.IsIndexExist(index) { - result = append(result, item.GetByIndex(index)) - } - } else { - if item.Get(t.Key).IsIndexExist(index) { - result = append(result, item.Get(t.Key).GetByIndex(index)) - } - } - } - } - } else { - for _, item := range data { - if t.Range.Start == -1 && t.Range.End == -1 { - if item.IsExist(t.Key) { - result = append(result, item.Get(t.Key)) - } - - continue - } - - RANGELOOP: - for index := t.Range.Start; index < t.Range.End; index++ { - if t.Key == "" { - if item.IsIndexExist(index) { - result = append(result, item.GetByIndex(index)) - } else { - break RANGELOOP - } - } else { - if item.Get(t.Key).IsIndexExist(index) { - result = append(result, item.Get(t.Key).GetByIndex(index)) - } else { - break RANGELOOP - } - } - } - } - } - - return result -} - -// execBasicTokenSelector executes basic query token over given data -func execBasicTokenSelector(t Token, data []*simpleyaml.Yaml) []*simpleyaml.Yaml { - var result []*simpleyaml.Yaml - - for _, item := range data { - if item.IsExist(t.Key) { - result = append(result, item.Get(t.Key)) - } - } - - return result -} - -// renderData renders yaml structs to string -func renderData(data []*simpleyaml.Yaml) { - for _, item := range data { - switch { - case item.IsArray(): - if item.GetByIndex(0).IsMap() || item.GetByIndex(0).IsArray() { - encodeYaml(item) - } else { - fmt.Println(strings.Join(item.MustStringArray(nil), "\n")) - } - - case item.IsMap(): - encodeYaml(item) - - default: - fmt.Printf("%v\n", item.Interface()) - } - } -} - -// processData runs processors over given data -func processData(processor []string, data []*simpleyaml.Yaml) { - var result interface{} - - for _, pf := range processor { - switch pf { - case "len", "length": - result = processorFuncLength(data, result) - case "keys": - result = processorFuncKeys(data, result) - case "sort": - result = processorFuncSort(result) - default: - printError("Unknown function \"%s\"", pf) - os.Exit(1) - } - } - - switch result.(type) { - case string, int: - fmt.Println(result) - case []int: - for _, v := range result.([]int) { - fmt.Println(v) - } - case []string: - for _, v := range result.([]string) { - fmt.Println(v) - } - } -} - -// processorFuncLength is a length processor -func processorFuncLength(data []*simpleyaml.Yaml, k interface{}) []int { - var result []int - - if k == nil { - for _, item := range data { - switch { - case item.IsArray(): - result = append(result, len(item.MustArray(nil))) - case item.IsMap(): - result = append(result, len(item.MustMap(nil))) - default: - result = append(result, len(item.MustString(""))) - } - } - } else { - switch k.(type) { - case string: - return []int{len(k.(string))} - case []string: - return []int{len(k.([]string))} - } - } - - return result -} - -// processorFuncKeys is a keys processor -func processorFuncKeys(data []*simpleyaml.Yaml, k interface{}) []string { - var result []string - - if k != nil { - return nil - } - - for _, item := range data { - if item.IsMap() { - keys, _ := item.GetMapKeys() - result = append(result, keys...) - } - } - - return result -} - -// processorFuncKeys is a sort processor -func processorFuncSort(k interface{}) []string { - var result []string - - switch k.(type) { - case string: - result = []string{k.(string)} - case []string: - result = k.([]string) - sort.Strings(result) - } - - return result -} - -// parseQuery parses query -func parseQuery(query string) []Query { - var result []Query - - for _, q := range splitQuery(query) { - result = append(result, parseSubQuery(q)) - } - - return result -} - -// parseSubQuery parses sub-query -func parseSubQuery(query string) Query { - query = strings.TrimSpace(query) - - if !strings.Contains(query, "|") { - return Query{Tokens: parseTokens(query)} - } - - qs := strings.Split(query, "|") - - if len(qs) < 2 { - return Query{Tokens: parseTokens(qs[0])} - } - - return Query{Tokens: parseTokens(qs[0]), Processors: parseProcessors(qs[1:])} -} - -// parseTokens splits query to tokens -func parseTokens(query string) []Token { - query = strings.TrimSpace(query) - - var result []Token - - for i, t := range strings.Split(query, ".") { - if i == 0 || t == "" { - continue - } - - result = append(result, parseToken(t)) - } - - return result -} - -// parseToken parses token -func parseToken(token string) Token { - if strings.Contains(token, "[") && strings.Contains(token, "]") { - is := strings.Index(token, "[") - return parseArrayToken(token[:is], token[is:]) - } - - return Token{Key: token, Range: Range{-1, -1}} -} - -// parseArrayToken parses array token -func parseArrayToken(key, index string) Token { - if index == "[]" { - return Token{Key: key, Range: Range{0, 999999999}} - } - - index = strings.TrimLeft(index, "[") - index = strings.TrimRight(index, "]") - - if strings.Contains(index, ":") { - is := strings.Split(index, ":") - - return Token{ - Key: key, - Range: Range{ - str2int(is[0], 0), - str2int(is[1], 999999999), - }, - } - } else if strings.Contains(index, ",") { - return Token{Key: key, Range: Range{-1, -1}, Index: converEnum(strings.Split(index, ","))} - } else { - return Token{Key: key, Range: Range{-1, -1}, Index: []int{str2int(index, 0)}} - } -} - -// parseProcessors parses processors -func parseProcessors(processors []string) []string { - var result []string - - for _, p := range processors { - result = append(result, strings.TrimSpace(p)) - } - - return result -} - -// splitQuery splits query -func splitQuery(query string) []string { - var result []string - var buffer string - var isArray bool - - for _, r := range query { - switch r { - case '[': - isArray = true - case ']': - isArray = false - } - - if r == ',' && !isArray { - result = append(result, buffer) - buffer = "" - continue - } - - buffer += string(r) - } - - if buffer != "" { - result = append(result, buffer) - } - - return result -} - -// converEnum converts string slice to int slice -func converEnum(s []string) []int { - var result []int - - for _, i := range s { - result = append(result, str2int(i, 0)) - } - - return result -} - -// str2int converts string to int -func str2int(s string, def int) int { - s = strings.TrimSpace(s) - - if s == "" { - return def - } - - i, _ := strconv.Atoi(s) - - return i -} - -// encodeYaml encodes yaml struct to string -func encodeYaml(yaml *simpleyaml.Yaml) { - data, _ := yaml.MarshalYAML() - - // Print encoded YAML without new line symbol - fmt.Println(string(data[:len(data)-1])) -} - -// printError prints error message to console -func printError(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// IsArrayToken returns true if it array selector token -func (t Token) IsArrayToken() bool { - if len(t.Index) != 0 || t.Range.Start != -1 || t.Range.End != -1 { - return true - } - - return false -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// showUsage prints usage info -func showUsage() { - genUsage().Render() -} - -// showAbout prints info about version -func showAbout() { - genAbout().Render() -} - -// genCompletion generates completion for different shells -func genCompletion() int { - info := genUsage() - - switch options.GetS(OPT_COMPLETION) { - case "bash": - fmt.Printf(bash.Generate(info, "yo")) - case "fish": - fmt.Printf(fish.Generate(info, "yo")) - case "zsh": - fmt.Printf(zsh.Generate(info, optMap, "yo")) - default: - return 1 - } - - return 0 -} - -// genMan generates man page -func genMan() int { - fmt.Println( - man.Generate( - genUsage(), - genAbout(), - ), - ) - - return 0 -} - -// genUsage generates usage info -func genUsage() *usage.Info { - info := usage.NewInfo("", "query") - - info.AddOption(OPT_FROM_FILE, "Read data from file", "filename") - info.AddOption(OPT_NO_COLOR, "Disable colors in output") - info.AddOption(OPT_HELP, "Show this help message") - info.AddOption(OPT_VER, "Show version") - - info.AddRawExample("cat file.yml | yo '.foo'", "Return value for key foo") - info.AddExample("-f file.yml '.foo'", "Return value for key foo") - info.AddExample("-f file.yml '.foo | length'", "Print value length") - info.AddExample("-f file.yml '.foo[]'", "Return all items from array") - info.AddExample("-f file.yml '.bar[2:]'", "Return subarray started from item with index 2") - info.AddExample("-f file.yml '.bar[1,2,5]'", "Return items with index 1, 2 and 5 from array") - info.AddExample("-f file.yml '.bar[] | length'", "Print array size") - info.AddExample("-f file.yml '.xyz | keys'", "Print hash map keys") - info.AddExample("-f file.yml '.xyz | keys | length'", "Print number of hash map keys") - info.AddExample("-f file.yml '.xyz | keys | sort'", "Print sorted list of keys") - - return info -} - -// genAbout generates info about version -func genAbout() *usage.About { - about := &usage.About{ - App: APP, - Version: VER, - Desc: DESC, - Year: 2006, - Owner: "ESSENTIAL KAOS", - License: "Apache License, Version 2.0 ", - UpdateChecker: usage.UpdateChecker{"essentialkaos/yo", update.GitHubChecker}, - } - - return about + CLI.Run(gitrev, gomod) }