Skip to content

Commit

Permalink
Merge pull request #13 from graugans/feature/templates
Browse files Browse the repository at this point in the history
Add the possibility to filter the get command with the go text/template package
  • Loading branch information
graugans authored May 15, 2024
2 parents c1172a3 + 3c2d4d8 commit 9347fc1
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 5 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,29 @@ A GO module and cli to access the ifm OVP8xx series of devices.

This project is still a work in progress and will suffer from breaking API changes. Please be warned. In case you have any suggestions or want to contribute please feel free to open an issue or pull request.

## CLI Installation
## CLI

### Pre Build Binaries
One of the benefits of the Go language is the easy way of producing statically linked binaries to be used on all major platforms. One of the `ovp8xx` core features is the CLI interface. The design tries to stay as close as possible to the XML-RPC API.

With the CLI you can get the configuration from the device and [filter](doc/filter.md) it as you need. After transforming the config it can be written back to the device.

### CLI Installation

#### Pre Build Binaries

The recommended and easiest way is to download the pre-build binary from the [GitHub Release page](https://github.com/graugans/go-ovp8xx/releases).

⚠️ The Windows binary maybe flagged by a Virus Scanner, please also read the note from the [Go Team](https://go.dev/doc/faq#virus) ⚠️

### Go get
#### Go get

If you have a decent Go version installed

```sh
go install github.com/graugans/go-ovp8xx/v2/cmd/ovp8xx@latest
```

## API usage
### API usage

Within in your Go project get the ovp8xx package first

Expand Down
60 changes: 60 additions & 0 deletions cmd/ovp8xx/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,40 @@ Copyright © 2023 Christian Ege <[email protected]>
package cmd

import (
"encoding/json"
"errors"
"fmt"
"os"
"text/template"

"github.com/graugans/go-ovp8xx/v2/pkg/ovp8xx"
"github.com/spf13/cobra"
)

// toJSON converts the given object to a JSON string representation.
// If an error occurs during marshaling, an empty string is returned.
// This is taken from https://github.com/intel/tfortools
func toJSON(obj interface{}) string {
b, err := json.MarshalIndent(obj, "", "\t")
if err != nil {
return ""
}
return string(b)
}

// prefix can be used to create a list separated by s and the very first
// element is not prefixed.
func prefix(s string) func() string {
i := -1
return func() string {
i++
if i == 0 {
return ""
}
return s
}
}

func getCommand(cmd *cobra.Command, args []string) error {
var result ovp8xx.Config
var err error
Expand All @@ -23,6 +53,35 @@ func getCommand(cmd *cobra.Command, args []string) error {
if result, err = o3r.Get(helper.jsonPointers()); err != nil {
return err
}

if cmd.Flags().Changed("format") {
if helper.prettyPrint() {
return errors.New("you can't use --pretty and --format at the same time")
}

format, err := cmd.Flags().GetString("format")
if err != nil {
return fmt.Errorf("unable to get the format string from the command line: %w", err)
}
var inputData interface{}
if err = json.Unmarshal([]byte(result.String()), &inputData); err != nil {
return fmt.Errorf("unable to unmarshal the JSON data from the 'get' call: %w", err)
}
templateFunctions := template.FuncMap{
"toJSON": toJSON,
"prefix": prefix,
}
tmpl, err := template.New("output").Funcs(templateFunctions).Parse(format)
if err != nil {
return fmt.Errorf("unable to parse the template: %w", err)
}

if err := tmpl.Execute(os.Stdout, inputData); err != nil {
return fmt.Errorf("unable to execute the template: %w", err)
}
return nil
}

if err := helper.printJSONResult(result.String()); err != nil {
return err
}
Expand Down Expand Up @@ -66,4 +125,5 @@ func init() {
rootCmd.AddCommand(getCmd)
getCmd.Flags().StringSliceP("pointer", "p", []string{""}, "A JSON pointer to be queried")
getCmd.Flags().Bool("pretty", false, "Pretty print the JSON received from the device")
getCmd.Flags().String("format", "", "Specify an alternative format for the JSON output")
}
7 changes: 6 additions & 1 deletion cmd/ovp8xx/cmd/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type helperConfig struct {

func (c *helperConfig) printJSONResult(data string) error {
var message string = data
if c.pretty {
if c.prettyPrint() {
var js json.RawMessage
if err := json.Unmarshal([]byte(data), &js); err != nil {
return errors.New("malformed json")
Expand All @@ -44,6 +44,11 @@ func (c *helperConfig) remotePort() uint16 {
return c.port
}

// prettyPrint returns a boolean value indicating whether the output should be pretty-printed.
func (c *helperConfig) prettyPrint() bool {
return c.pretty
}

func NewHelper(cmd *cobra.Command) (helperConfig, error) {
var conf = helperConfig{}
var err error
Expand Down
3 changes: 3 additions & 0 deletions cmd/ovp8xx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/spf13/cobra"
)

// SetVersionInfo sets the version information for the root command.
// It formats the version, commit, and date into a string and assigns it to the rootCmd.Version variable.
func SetVersionInfo(version, commit, date string) {
rootCmd.Version = fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit)
}
Expand All @@ -33,4 +35,5 @@ func Execute() {

func init() {
rootCmd.PersistentFlags().String("ip", ovp8xx.GetEnv("OVP8XX_IP", "192.168.0.69"), "The IP address or hostname of the OVP8XX. If not provided the default will be taken from the environment variable OVP8XX_IP")

}
10 changes: 10 additions & 0 deletions cmd/ovp8xx/ovp8xx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"github.com/graugans/go-ovp8xx/v2/cmd/ovp8xx/cmd"
"github.com/graugans/go-ovp8xx/v2/internal/versioninfo"
)

var (
Expand All @@ -11,6 +12,15 @@ var (
)

func main() {
// If the version is "dev", it means that the binary is built using "go install",
// "go build" or "go run".
// However, if the binary is build by Goreleaser we use that version.
if version == "dev" {
version = versioninfo.Version
commit = versioninfo.Revision
date = versioninfo.LastCommit.String()
}

cmd.SetVersionInfo(
version,
commit,
Expand Down
92 changes: 92 additions & 0 deletions doc/filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Using Go text/template for Advanced Data Filtering

This document demonstrates the usage of the Go text/template package to filter and format the output of the `ovp8xx get` command for manipulating the result data. There are two custom functions created to enhance the filtering: `prefix` and `toJSON`.

## The `prefix` Function

The `prefix` function is designed to solve the problem of trailing commas, which are, for example, not allowed in JSON. The idea is to prefix each line with a comma, except for the very first one. This function is particularly useful when generating JSON output dynamically, where the number of elements may vary.

Here's how it's used in the command:

```sh
ovp8xx get --format '{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{call $p}}"{{- $key -}}":{"state": "{{ .state }}"}{{- end }} } }'
```

In this command, `{{$p := prefix ", "}}` initializes the prefix function with a comma and a space. Then, `{{call $p}}` is used to insert the prefix before each port entry. The prefix function ensures that no comma is inserted before the first entry.

The result is a JSON object where each line, except the first, is prefixed with a comma:

```sh
{
"ports": {
"port0": {
"state": "RUN"
},
"port1": {
"state": "RUN"
},
"port2": {
"state": "RUN"
},
"port3": {
"state": "RUN"
},
"port6": {
"state": "RUN"
}
}
}
```

## Print all ports with some details

The following command retrieves all port data and formats it for easy reading:

```sh
ovp8xx get --format '{{$p := prefix "\n"}}{{ range $port, $details := .ports }}{{call $p}}[{{ $port }}] state: {{ $details.state }},{{print "\t"}}type: {{ $details.info.features.type }},{{print "\t"}}PCIC Port: {{ $details.data.pcicTCPPort }}{{ end }}'
```

The result will look like:

```
[port0] state: RUN, type: 3D, PCIC Port: 50010
[port1] state: RUN, type: 3D, PCIC Port: 50011
[port2] state: RUN, type: 2D, PCIC Port: 50012
[port3] state: RUN, type: 2D, PCIC Port: 50013
[port6] state: RUN, type: IMU, PCIC Port: 50016
```

## Modify the state of the ports

Now we are taking the example from the [`prefix`](#the-prefix-function) function and enhance it to also modify the state of all ports (except port6) to CONF with the following command:

```sh
ovp8xx get --format '{{ $state := "CONF" }}{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{ if ne $key "port6" }}{{call $p}}"{{- $key -}}":{"state": "{{ $state }}"}{{end}}{{- end }} } }'
```

The output will be a JSON object that can be directly piped into the set command:

```sh
{
"ports": {
"port0": {
"state": "CONF"
},
"port1": {
"state": "CONF"
},
"port2": {
"state": "CONF"
},
"port3": {
"state": "CONF"
}
}
}
```

To directly apply those changes to the device, use the following command:

```sh
ovp8xx get --format '{{ $state := "CONF" }}{ "ports": { {{$p := prefix ", "}}{{- range $key, $val := .ports -}}{{ if ne $key "port6" }}{{call $p}}"{{- $key -}}":{"state": "{{ $state }}"}{{end}}{{- end }} } }' | ovp8xx set
```
46 changes: 46 additions & 0 deletions internal/versioninfo/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Package versioninfo uses runtime.ReadBuildInfo() to set global executable revision information if possible.
package versioninfo

// SPDX-License-Identifier: MIT
// Copyright (c) 2021 Carl Johnson
// Based on https://github.com/earthboundkid/versioninfo

import (
"runtime/debug"
"time"
)

var (
// Version will be the version tag if the binary is built with "go install url/tool@version".
// If the binary is built some other way, it will be "(devel)".
Version = "unknown"
// Revision is taken from the vcs.revision tag in Go 1.18+.
Revision = "unknown"
// LastCommit is taken from the vcs.time tag in Go 1.18+.
LastCommit time.Time
// DirtyBuild is taken from the vcs.modified tag in Go 1.18+.
DirtyBuild = true
)

func init() {
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
for _, kv := range info.Settings {
if kv.Value == "" {
continue
}
switch kv.Key {
case "vcs.revision":
Revision = kv.Value
case "vcs.time":
LastCommit, _ = time.Parse(time.RFC3339, kv.Value)
case "vcs.modified":
DirtyBuild = kv.Value == "true"
}
}
if info.Main.Version != "" {
Version = info.Main.Version
}
}

0 comments on commit 9347fc1

Please sign in to comment.