mirror of
https://github.com/neilotoole/sq.git
synced 2024-11-28 12:33:44 +03:00
Improvements to 'sq version' command (#137)
This commit is contained in:
parent
13cfc0e1c8
commit
66a8c39844
11
CHANGELOG.md
11
CHANGELOG.md
@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v0.21.0] - 2022-12-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `sq version` respects `--json` flag.
|
||||||
|
- `sq version` respects `--verbose` flag. It also shows less info when `-v` is not set.
|
||||||
|
- `sq version` shows `latest_version` info when `--verbose` and there's a newer version available.
|
||||||
|
|
||||||
## [v0.20.0] - 2022-12-29
|
## [v0.20.0] - 2022-12-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -92,7 +100,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#89]: Bug with SQL generated for joins.
|
- [#89]: Bug with SQL generated for joins.
|
||||||
|
|
||||||
|
|
||||||
[v0.19.0]: https://github.com/neilotoole/sq/compare/v0.19.0...v0.20.0
|
[v0.21.0]: https://github.com/neilotoole/sq/compare/v0.20.0...v0.21.0
|
||||||
|
[v0.20.0]: https://github.com/neilotoole/sq/compare/v0.19.0...v0.20.0
|
||||||
[v0.19.0]: https://github.com/neilotoole/sq/compare/v0.18.2...v0.19.0
|
[v0.19.0]: https://github.com/neilotoole/sq/compare/v0.18.2...v0.19.0
|
||||||
[v0.18.2]: https://github.com/neilotoole/sq/compare/v0.18.0...v0.18.2
|
[v0.18.2]: https://github.com/neilotoole/sq/compare/v0.18.0...v0.18.2
|
||||||
[v0.18.0]: https://github.com/neilotoole/sq/compare/v0.17.0...v0.18.0
|
[v0.18.0]: https://github.com/neilotoole/sq/compare/v0.17.0...v0.18.0
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
// Package buildinfo hosts build info variables populated via ldflags.
|
// Package buildinfo hosts build info variables populated via ldflags.
|
||||||
package buildinfo
|
package buildinfo
|
||||||
|
|
||||||
// defaultVersion is the default value for Version if not
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/libsq/core/stringz"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultVersion is the default value for Version if not
|
||||||
// set via ldflags.
|
// set via ldflags.
|
||||||
const defaultVersion = "v0.0.0-dev"
|
const DefaultVersion = "v0.0.0-dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Version is the build version. If not set at build time via
|
// Version is the build version. If not set at build time via
|
||||||
// ldflags, Version takes the value of defaultVersion.
|
// ldflags, Version takes the value of DefaultVersion.
|
||||||
Version = defaultVersion
|
Version = DefaultVersion
|
||||||
|
|
||||||
// Commit is the commit hash.
|
// Commit is the commit hash.
|
||||||
Commit string
|
Commit string
|
||||||
@ -16,3 +24,56 @@ var (
|
|||||||
// Timestamp is the timestamp of when the cli was built.
|
// Timestamp is the timestamp of when the cli was built.
|
||||||
Timestamp string
|
Timestamp string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BuildInfo encapsulates Version, Commit and Timestamp.
|
||||||
|
type BuildInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Commit string `json:"commit,omitempty"`
|
||||||
|
Timestamp string `json:"timestamp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of BuildInfo.
|
||||||
|
func (bi BuildInfo) String() string {
|
||||||
|
s := bi.Version
|
||||||
|
if bi.Commit != "" {
|
||||||
|
s += " " + bi.Commit
|
||||||
|
}
|
||||||
|
if bi.Timestamp != "" {
|
||||||
|
s += " " + bi.Timestamp
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns BuildInfo.
|
||||||
|
func Info() BuildInfo {
|
||||||
|
return BuildInfo{
|
||||||
|
Version: Version,
|
||||||
|
Commit: Commit,
|
||||||
|
Timestamp: Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { //nolint:gochecknoinits
|
||||||
|
if strings.HasSuffix(Version, "~dev") {
|
||||||
|
Version = strings.Replace(Version, "~dev", "-dev", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Version != "" && !semver.IsValid(Version) {
|
||||||
|
// We want to panic here because it is a pipeline/build failure
|
||||||
|
// to have an invalid non-empty Version.
|
||||||
|
panic(fmt.Sprintf("Invalid BuildInfo.Version value: %q", Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
if Timestamp != "" {
|
||||||
|
// Make sure Timestamp is normalized
|
||||||
|
t := stringz.TimestampToRFC3339(Timestamp)
|
||||||
|
if t != "" {
|
||||||
|
Timestamp = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDefaultVersion returns true if Version is empty or DefaultVersion.
|
||||||
|
func IsDefaultVersion() bool {
|
||||||
|
return Version == "" || Version == DefaultVersion
|
||||||
|
}
|
||||||
|
@ -555,6 +555,7 @@ type writers struct {
|
|||||||
srcw output.SourceWriter
|
srcw output.SourceWriter
|
||||||
errw output.ErrorWriter
|
errw output.ErrorWriter
|
||||||
pingw output.PingWriter
|
pingw output.PingWriter
|
||||||
|
versionw output.VersionWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWriters returns a writers instance configured per defaults and/or
|
// newWriters returns a writers instance configured per defaults and/or
|
||||||
@ -577,6 +578,7 @@ func newWriters(log lg.Log, cmd *cobra.Command, defaults config.Defaults, out, e
|
|||||||
srcw: tablew.NewSourceWriter(out2, fm),
|
srcw: tablew.NewSourceWriter(out2, fm),
|
||||||
pingw: tablew.NewPingWriter(out2, fm),
|
pingw: tablew.NewPingWriter(out2, fm),
|
||||||
errw: tablew.NewErrorWriter(errOut2, fm),
|
errw: tablew.NewErrorWriter(errOut2, fm),
|
||||||
|
versionw: tablew.NewVersionWriter(out2, fm),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke getFormat to see if the format was specified
|
// Invoke getFormat to see if the format was specified
|
||||||
@ -589,6 +591,7 @@ func newWriters(log lg.Log, cmd *cobra.Command, defaults config.Defaults, out, e
|
|||||||
w.recordw = jsonw.NewStdRecordWriter(out2, fm)
|
w.recordw = jsonw.NewStdRecordWriter(out2, fm)
|
||||||
w.metaw = jsonw.NewMetadataWriter(out2, fm)
|
w.metaw = jsonw.NewMetadataWriter(out2, fm)
|
||||||
w.errw = jsonw.NewErrorWriter(log, errOut2, fm)
|
w.errw = jsonw.NewErrorWriter(log, errOut2, fm)
|
||||||
|
w.versionw = jsonw.NewVersionWriter(out2, fm)
|
||||||
|
|
||||||
case config.FormatTable:
|
case config.FormatTable:
|
||||||
// Table is the base format, already set above, no need to do anything.
|
// Table is the base format, already set above, no need to do anything.
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/libsq/core/errz"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
|
||||||
"github.com/neilotoole/sq/cli/buildinfo"
|
"github.com/neilotoole/sq/cli/buildinfo"
|
||||||
)
|
)
|
||||||
@ -15,23 +23,110 @@ func newVersionCmd() *cobra.Command {
|
|||||||
RunE: execVersion,
|
RunE: execVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP(flagJSON, flagJSONShort, false, flagJSONUsage)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func execVersion(cmd *cobra.Command, args []string) error {
|
func execVersion(cmd *cobra.Command, args []string) error {
|
||||||
rc := RunContextFrom(cmd.Context())
|
rc := RunContextFrom(cmd.Context())
|
||||||
rc.writers.fm.Hilite.Fprintf(rc.Out, "sq %s", buildinfo.Version)
|
|
||||||
|
|
||||||
if len(buildinfo.Commit) > 0 {
|
// We'd like to display that there's an update available, but
|
||||||
fmt.Fprint(rc.Out, " ")
|
// we don't want to wait around long for that.
|
||||||
rc.writers.fm.Faint.Fprint(rc.Out, "#"+buildinfo.Commit)
|
// So, we swallow (but log) any error from the goroutine.
|
||||||
|
ctx, cancelFn := context.WithTimeout(cmd.Context(), time.Second*2)
|
||||||
|
defer cancelFn()
|
||||||
|
|
||||||
|
resultCh := make(chan string)
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
v, err := fetchBrewVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
rc.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(buildinfo.Timestamp) > 0 {
|
// OK if v is empty
|
||||||
fmt.Fprint(rc.Out, " ")
|
resultCh <- v
|
||||||
rc.writers.fm.Faint.Fprint(rc.Out, buildinfo.Timestamp)
|
}()
|
||||||
|
|
||||||
|
var latestVersion string
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case latestVersion = <-resultCh:
|
||||||
|
if latestVersion != "" && !strings.HasPrefix(latestVersion, "v") {
|
||||||
|
latestVersion = "v" + latestVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(rc.Out)
|
return rc.writers.versionw.Version(buildinfo.Info(), latestVersion)
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
func fetchBrewVersion(ctx context.Context) (string, error) {
|
||||||
|
const u = `https://raw.githubusercontent.com/neilotoole/homebrew-sq/master/sq.rb`
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", u, http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", errz.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errz.Wrap(err, "failed to check edgectl brew repo")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if err != nil {
|
||||||
|
return "", errz.Errorf("failed to check edgectl brew repo: %d %s",
|
||||||
|
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", errz.Wrap(err, "failed to read edgectl brew repo body")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getVersionFromBrewFormula(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getVersionFromBrewFormula returns the first brew version
|
||||||
|
// from f, which is a brew ruby formula. The version is returned
|
||||||
|
// without a "v" prefix, e.g. "0.1.2", not "v0.1.2".
|
||||||
|
func getVersionFromBrewFormula(f []byte) (string, error) {
|
||||||
|
var (
|
||||||
|
line string
|
||||||
|
val string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
sc := bufio.NewScanner(bytes.NewReader(f))
|
||||||
|
for sc.Scan() {
|
||||||
|
line = sc.Text()
|
||||||
|
if err = sc.Err(); err != nil {
|
||||||
|
return "", errz.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(val, `version "`) {
|
||||||
|
// found it
|
||||||
|
val = val[9:]
|
||||||
|
val = strings.TrimSuffix(val, `"`)
|
||||||
|
if !semver.IsValid("v" + val) { // semver pkg requires "v" prefix
|
||||||
|
return "", errz.Errorf("invalid brew formula: invalid semver")
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "bottle") {
|
||||||
|
// Gone too far
|
||||||
|
return "", errz.New("unable to parse brew formula")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc.Err() != nil {
|
||||||
|
return "", errz.Wrap(err, "invalid brew formula")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errz.New("invalid brew formula")
|
||||||
}
|
}
|
||||||
|
26
cli/cmd_version_test.go
Normal file
26
cli/cmd_version_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetVersionFromBrewFormula(t *testing.T) {
|
||||||
|
f, err := os.ReadFile("testdata/sq-0.20.0.rb")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
vers, err := getVersionFromBrewFormula(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "0.20.0", vers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchBrewVersion(t *testing.T) {
|
||||||
|
latest, err := fetchBrewVersion(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, semver.IsValid("v"+latest))
|
||||||
|
}
|
@ -3,15 +3,42 @@ package jsonw
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/neilotoole/sq/cli/output"
|
"github.com/neilotoole/sq/cli/output"
|
||||||
"github.com/neilotoole/sq/cli/output/jsonw/internal"
|
"github.com/neilotoole/sq/cli/output/jsonw/internal"
|
||||||
|
jcolorenc "github.com/neilotoole/sq/cli/output/jsonw/internal/jcolorenc"
|
||||||
"github.com/neilotoole/sq/libsq/core/errz"
|
"github.com/neilotoole/sq/libsq/core/errz"
|
||||||
"github.com/neilotoole/sq/libsq/core/sqlz"
|
"github.com/neilotoole/sq/libsq/core/sqlz"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// writeJSON prints a JSON representation of v to out, using specs
|
||||||
|
// from fm.
|
||||||
|
func writeJSON(out io.Writer, fm *output.Formatting, v any) error {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
enc := jcolorenc.NewEncoder(buf)
|
||||||
|
enc.SetColors(internal.NewColors(fm))
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
if fm.Pretty {
|
||||||
|
enc.SetIndent("", fm.Indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := enc.Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
return errz.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprint(out, buf.String())
|
||||||
|
if err != nil {
|
||||||
|
return errz.Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewStdRecordWriter returns a record writer that outputs each
|
// NewStdRecordWriter returns a record writer that outputs each
|
||||||
// record as a JSON object that is an element of JSON array. This is
|
// record as a JSON object that is an element of JSON array. This is
|
||||||
// to say, standard JSON. For example:
|
// to say, standard JSON. For example:
|
||||||
|
36
cli/output/jsonw/versionw.go
Normal file
36
cli/output/jsonw/versionw.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package jsonw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/cli/buildinfo"
|
||||||
|
"github.com/neilotoole/sq/cli/output"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ output.VersionWriter = (*versionWriter)(nil)
|
||||||
|
|
||||||
|
// versionWriter implements output.VersionWriter for JSON.
|
||||||
|
type versionWriter struct {
|
||||||
|
out io.Writer
|
||||||
|
fm *output.Formatting
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVersionWriter returns a new output.VersionWriter instance
|
||||||
|
// that outputs version info in JSON.
|
||||||
|
func NewVersionWriter(out io.Writer, fm *output.Formatting) output.VersionWriter {
|
||||||
|
return &versionWriter{out: out, fm: fm}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *versionWriter) Version(info buildinfo.BuildInfo, latestVersion string) error {
|
||||||
|
type cliBuildInfo struct {
|
||||||
|
buildinfo.BuildInfo
|
||||||
|
LatestVersion string `json:"latest_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
bi := cliBuildInfo{
|
||||||
|
BuildInfo: info,
|
||||||
|
LatestVersion: latestVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeJSON(w.out, w.fm, bi)
|
||||||
|
}
|
54
cli/output/tablew/versionw.go
Normal file
54
cli/output/tablew/versionw.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package tablew
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/cli/buildinfo"
|
||||||
|
"github.com/neilotoole/sq/cli/output"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ output.VersionWriter = (*versionWriter)(nil)
|
||||||
|
|
||||||
|
// versionWriter implements output.VersionWriter for JSON.
|
||||||
|
type versionWriter struct {
|
||||||
|
out io.Writer
|
||||||
|
fm *output.Formatting
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVersionWriter returns a new output.VersionWriter instance
|
||||||
|
// that outputs version info in JSON.
|
||||||
|
func NewVersionWriter(out io.Writer, fm *output.Formatting) output.VersionWriter {
|
||||||
|
return &versionWriter{out: out, fm: fm}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *versionWriter) Version(bi buildinfo.BuildInfo, latestVersion string) error {
|
||||||
|
fmt.Fprintf(w.out, "sq %s", bi.Version)
|
||||||
|
|
||||||
|
// Only print more if --verbose is set.
|
||||||
|
if !w.fm.Verbose {
|
||||||
|
fmt.Fprintln(w.out)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bi.Commit) > 0 {
|
||||||
|
fmt.Fprint(w.out, " ")
|
||||||
|
w.fm.Faint.Fprint(w.out, "#"+bi.Commit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bi.Timestamp) > 0 {
|
||||||
|
fmt.Fprint(w.out, " ")
|
||||||
|
w.fm.Faint.Fprint(w.out, bi.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
showUpdate := semver.Compare(latestVersion, bi.Version) > 0
|
||||||
|
if showUpdate {
|
||||||
|
fmt.Fprint(w.out, " ")
|
||||||
|
w.fm.Faint.Fprint(w.out, "Update available: ")
|
||||||
|
w.fm.Number.Fprint(w.out, latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(w.out)
|
||||||
|
return nil
|
||||||
|
}
|
@ -9,6 +9,7 @@ package output
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/neilotoole/sq/cli/buildinfo"
|
||||||
"github.com/neilotoole/sq/libsq/core/sqlz"
|
"github.com/neilotoole/sq/libsq/core/sqlz"
|
||||||
"github.com/neilotoole/sq/libsq/driver"
|
"github.com/neilotoole/sq/libsq/driver"
|
||||||
"github.com/neilotoole/sq/libsq/source"
|
"github.com/neilotoole/sq/libsq/source"
|
||||||
@ -81,6 +82,14 @@ type PingWriter interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VersionWriter prints the CLI version.
|
||||||
|
type VersionWriter interface {
|
||||||
|
// Version prints version info. Arg latestVersion is the latest
|
||||||
|
// version available from the homebrew repository. The value
|
||||||
|
// may be empty.
|
||||||
|
Version(info buildinfo.BuildInfo, latestVersion string) error
|
||||||
|
}
|
||||||
|
|
||||||
// FlushThreshold is the size in bytes after which a writer
|
// FlushThreshold is the size in bytes after which a writer
|
||||||
// should flush any internal buffer.
|
// should flush any internal buffer.
|
||||||
const FlushThreshold = 1000
|
const FlushThreshold = 1000
|
||||||
|
68
cli/testdata/sq-0.20.0.rb
vendored
Normal file
68
cli/testdata/sq-0.20.0.rb
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# This file was generated by GoReleaser. DO NOT EDIT.
|
||||||
|
class Sq < Formula
|
||||||
|
desc "sq: swiss-army knife for data"
|
||||||
|
homepage "https://github.com/neilotoole/sq"
|
||||||
|
version "0.20.0"
|
||||||
|
license "MIT"
|
||||||
|
|
||||||
|
on_macos do
|
||||||
|
if Hardware::CPU.arm?
|
||||||
|
url "https://github.com/neilotoole/sq/releases/download/v0.20.0/sq-0.20.0-macos-arm64.tar.gz"
|
||||||
|
sha256 "3804a2cbb7789df4d0008af45dea59969a932b242ec7f9e18c71deeb4132d4b5"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "sq"
|
||||||
|
bash_completion.install "completions/sq.bash" => "sq"
|
||||||
|
zsh_completion.install "completions/sq.zsh" => "_sq"
|
||||||
|
fish_completion.install "completions/sq.fish"
|
||||||
|
man1.install "manpages/sq.1.gz"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if Hardware::CPU.intel?
|
||||||
|
url "https://github.com/neilotoole/sq/releases/download/v0.20.0/sq-0.20.0-macos-amd64.tar.gz"
|
||||||
|
sha256 "5674316e5a8efd4430a30679cf5129e5c2e9966e30d294ba0901266ce8c9e212"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "sq"
|
||||||
|
bash_completion.install "completions/sq.bash" => "sq"
|
||||||
|
zsh_completion.install "completions/sq.zsh" => "_sq"
|
||||||
|
fish_completion.install "completions/sq.fish"
|
||||||
|
man1.install "manpages/sq.1.gz"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on_linux do
|
||||||
|
if Hardware::CPU.arm? && Hardware::CPU.is_64_bit?
|
||||||
|
url "https://github.com/neilotoole/sq/releases/download/v0.20.0/sq-0.20.0-linux-arm64.tar.gz"
|
||||||
|
sha256 "0769b928a202cf0c7174b6e0fb4f086375600bba333e8bbc2ec45a8b8d43464d"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "sq"
|
||||||
|
bash_completion.install "completions/sq.bash" => "sq"
|
||||||
|
zsh_completion.install "completions/sq.zsh" => "_sq"
|
||||||
|
fish_completion.install "completions/sq.fish"
|
||||||
|
man1.install "manpages/sq.1.gz"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if Hardware::CPU.intel?
|
||||||
|
url "https://github.com/neilotoole/sq/releases/download/v0.20.0/sq-0.20.0-linux-amd64.tar.gz"
|
||||||
|
sha256 "38263083d09dc5c0f5dd1d3cf24daac90fa7b013c865be7c9192fe7eca439486"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "sq"
|
||||||
|
bash_completion.install "completions/sq.bash" => "sq"
|
||||||
|
zsh_completion.install "completions/sq.zsh" => "_sq"
|
||||||
|
fish_completion.install "completions/sq.fish"
|
||||||
|
man1.install "manpages/sq.1.gz"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
system "#{bin}/sq --version"
|
||||||
|
end
|
||||||
|
end
|
@ -402,3 +402,131 @@ func TrimLen(s string, maxLen int) string {
|
|||||||
|
|
||||||
return s[:maxLen]
|
return s[:maxLen]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RFC3339Milli is an RFC3339 format with millisecond precision.
|
||||||
|
RFC3339Milli = "2006-01-02T15:04:05.000Z07:00"
|
||||||
|
|
||||||
|
// RFC3339MilliZulu is the same as RFC3339Milli, but in zulu time.
|
||||||
|
RFC3339MilliZulu = "2006-01-02T15:04:05.000Z"
|
||||||
|
|
||||||
|
// rfc3339variant is a variant using "-0700" suffix.
|
||||||
|
rfc3339variant = "2006-01-02T15:04:05-0700"
|
||||||
|
|
||||||
|
// RFC3339Zulu is an RFC3339 format, in Zulu time.
|
||||||
|
RFC3339Zulu = "2006-01-02T15:04:05Z"
|
||||||
|
|
||||||
|
// ISO8601 is similar to RFC3339Milli, but doesn't have the colon
|
||||||
|
// in the timezone offset.
|
||||||
|
ISO8601 = "2006-01-02T15:04:05.000Z0700"
|
||||||
|
|
||||||
|
// DateOnly is a date-only format.
|
||||||
|
DateOnly = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimestampUTC returns the RFC3339Milli representation of t in UTC.
|
||||||
|
func TimestampUTC(t time.Time) string {
|
||||||
|
return t.UTC().Format(RFC3339Milli)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DateUTC returns a date representation (2020-10-31) of t in UTC.
|
||||||
|
func DateUTC(t time.Time) string {
|
||||||
|
return t.UTC().Format(DateOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimestampToRFC3339 takes a RFC3339Milli, ISO8601 or RFC3339
|
||||||
|
// timestamp, and returns RFC3339. That is, the milliseconds are dropped.
|
||||||
|
// On error, the empty string is returned.
|
||||||
|
func TimestampToRFC3339(s string) string {
|
||||||
|
t, err := ParseTimestampUTC(s)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.UTC().Format(RFC3339Zulu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimestampToDate takes a RFC3339Milli, ISO8601 or RFC3339
|
||||||
|
// timestamp, and returns just the date component.
|
||||||
|
// On error, the empty string is returned.
|
||||||
|
func TimestampToDate(s string) string {
|
||||||
|
t, err := ParseTimestampUTC(s)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.UTC().Format(DateOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTimestampUTC is the counterpart of TimestampUTC. It attempts
|
||||||
|
// to parse s first in RFC3339Milli, then time.RFC3339 format, falling
|
||||||
|
// back to the subtly different ISO8601 format.
|
||||||
|
func ParseTimestampUTC(s string) (time.Time, error) {
|
||||||
|
t, err := time.Parse(RFC3339Milli, s)
|
||||||
|
if err == nil {
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to RFC3339
|
||||||
|
t, err = time.Parse(time.RFC3339, s)
|
||||||
|
if err == nil {
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to ISO8601
|
||||||
|
t, err = time.Parse(ISO8601, s)
|
||||||
|
if err == nil {
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err = time.Parse(rfc3339variant, s)
|
||||||
|
if err == nil {
|
||||||
|
return t.UTC(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, errz.Errorf("failed to parse timestamp {%s}", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLocalDate accepts a date string s, returning the local midnight
|
||||||
|
// time of that date. Arg s must in format "2006-01-02".
|
||||||
|
func ParseLocalDate(s string) (time.Time, error) {
|
||||||
|
if !strings.ContainsRune(s, 'T') {
|
||||||
|
// It's a date
|
||||||
|
t, err := time.ParseInLocation("2006-01-02", s, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's a 'T' in s, which means its probably a timestamp.
|
||||||
|
return time.Time{}, errz.Errorf("invalid date format: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUTCDate accepts a date string s, returning the UTC midnight
|
||||||
|
// time of that date. Arg s must in format "2006-01-02".
|
||||||
|
func ParseUTCDate(s string) (time.Time, error) {
|
||||||
|
if !strings.ContainsRune(s, 'T') {
|
||||||
|
// It's a date
|
||||||
|
t, err := time.ParseInLocation("2006-01-02", s, time.UTC)
|
||||||
|
if err != nil {
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's a 'T' in s, which means its probably a timestamp.
|
||||||
|
return time.Time{}, errz.Errorf("invalid date format: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDateOrTimestampUTC attempts to parse s as either
|
||||||
|
// a date (see ParseUTCDate), or timestamp (see ParseTimestampUTC).
|
||||||
|
// The returned time is in UTC.
|
||||||
|
func ParseDateOrTimestampUTC(s string) (time.Time, error) {
|
||||||
|
if strings.ContainsRune(s, 'T') {
|
||||||
|
return ParseTimestampUTC(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := ParseUTCDate(s)
|
||||||
|
return t.UTC(), err
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package stringz_test
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/neilotoole/sq/testh/tutil"
|
"github.com/neilotoole/sq/testh/tutil"
|
||||||
|
|
||||||
@ -303,3 +304,54 @@ func TestLineCount(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTimestampUTC(t *testing.T) {
|
||||||
|
tm := time.Date(2021, 0o1, 0o1, 7, 7, 7, 0, time.UTC)
|
||||||
|
s := stringz.TimestampUTC(tm)
|
||||||
|
t.Log(s)
|
||||||
|
require.Equal(t, "2021-01-01T07:07:07.000Z", s)
|
||||||
|
|
||||||
|
s = stringz.TimestampUTC(time.Now().UTC())
|
||||||
|
t.Log(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDateOrTimestampUTC(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
in string
|
||||||
|
want int64
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{in: "", wantErr: true},
|
||||||
|
{in: "not_a_time", wantErr: true},
|
||||||
|
{in: "2021-01-16T18:18:49.348-0700", want: 1610846329},
|
||||||
|
{in: "2021-01-16T18:26:39.216-07:00", want: 1610846799},
|
||||||
|
{in: "2021-01-16T18:26:39-07:00", want: 1610846799},
|
||||||
|
{in: "2021-01-17T01:26:39.216Z", want: 1610846799},
|
||||||
|
{in: "2021-01-17", want: 1610841600},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tutil.Name(i, tc.in), func(t *testing.T) {
|
||||||
|
tm, err := stringz.ParseDateOrTimestampUTC(tc.in)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("Got: %s", stringz.TimestampUTC(tm))
|
||||||
|
ut := tm.Unix()
|
||||||
|
require.Equal(t, tc.want, ut)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZuluTimestamp(t *testing.T) {
|
||||||
|
const (
|
||||||
|
input = `2022-12-30T09:36:31-0700`
|
||||||
|
want = `2022-12-30T16:36:31Z`
|
||||||
|
)
|
||||||
|
got := stringz.TimestampToRFC3339(input)
|
||||||
|
require.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
15
testh/post-install-macos.sh
Executable file
15
testh/post-install-macos.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ".completions.sh regenerates completions to "./completions".
|
||||||
|
|
||||||
|
set +e
|
||||||
|
brew uninstall neilotoole/sq/sq
|
||||||
|
|
||||||
|
if [[ $(which sq) ]]; then
|
||||||
|
echo "sq is still present"
|
||||||
|
rm $(which sq)
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
brew install neilotoole/sq/sq
|
||||||
|
|
||||||
|
# TODO: Test the version
|
Loading…
Reference in New Issue
Block a user