sort-cli: rework
This commit is contained in:
50
sort-cli/Makefile
Normal file
50
sort-cli/Makefile
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# HELPERS
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## help: print this help message
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo 'Usage:'
|
||||||
|
@sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
|
||||||
|
|
||||||
|
## all: tidy + audit + test/cover
|
||||||
|
all: tidy audit test/cover
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# QUALITY CONTROL
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## tidy: format code and tidy modfile
|
||||||
|
.PHONY: tidy
|
||||||
|
tidy:
|
||||||
|
go fmt ./...
|
||||||
|
go mod tidy -v
|
||||||
|
|
||||||
|
## audit: run quality control checks
|
||||||
|
.PHONY: audit
|
||||||
|
audit:
|
||||||
|
go mod verify
|
||||||
|
go vet ./...
|
||||||
|
go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./...
|
||||||
|
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
|
||||||
|
go test -race -buildvcs -vet=off ./...
|
||||||
|
go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run -v ./...
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================================================== #
|
||||||
|
# DEVELOPMENT
|
||||||
|
# ==================================================================================== #
|
||||||
|
|
||||||
|
## test: run all tests
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -v -race -buildvcs ./...
|
||||||
|
|
||||||
|
## test/cover: run all tests and display coverage
|
||||||
|
.PHONY: test/cover
|
||||||
|
test/cover:
|
||||||
|
go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./...
|
||||||
|
go tool cover -html=/tmp/coverage.out
|
26
sort-cli/config.go
Normal file
26
sort-cli/config.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "flag"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Key int
|
||||||
|
Numeric bool
|
||||||
|
Reverse bool
|
||||||
|
Unique bool
|
||||||
|
Sources []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ParseFlags() {
|
||||||
|
flag.IntVar(&c.Key, "k", 0, "sort via column")
|
||||||
|
flag.BoolVar(&c.Numeric, "n", false, "compare according to string numerical value")
|
||||||
|
flag.BoolVar(&c.Reverse, "r", false, "reverse the result of comparisons")
|
||||||
|
flag.BoolVar(&c.Unique, "u", false, "output only the first of an equal run")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
c.Sources = flag.Args()
|
||||||
|
|
||||||
|
if len(c.Sources) == 0 {
|
||||||
|
c.Sources = []string{"-"}
|
||||||
|
}
|
||||||
|
}
|
80
sort-cli/content.go
Normal file
80
sort-cli/content.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type content [][]byte
|
||||||
|
|
||||||
|
func (c *content) Sort(reverse bool) {
|
||||||
|
slices.SortFunc(*c, func(a, b []byte) int {
|
||||||
|
if reverse {
|
||||||
|
a, b = b, a
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Compare(a, b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) Uniques() {
|
||||||
|
m := make(map[string]struct{}, len(*c))
|
||||||
|
|
||||||
|
*c = slices.DeleteFunc[content, []byte](*c, func(line []byte) bool {
|
||||||
|
if _, ok := m[string(line)]; !ok {
|
||||||
|
m[string(line)] = struct{}{}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) String() string {
|
||||||
|
lines := *c
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for _, line := range lines {
|
||||||
|
n += len(line)
|
||||||
|
}
|
||||||
|
n += len(lines) * len("\n")
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(n)
|
||||||
|
|
||||||
|
for i, line := range lines {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rn := range line {
|
||||||
|
sb.WriteByte(rn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) Load(r io.Reader) {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, _, err := br.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(io.EOF, err) {
|
||||||
|
*c = append(*c, line)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalf("can't read line: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = append(*c, line)
|
||||||
|
}
|
||||||
|
}
|
51
sort-cli/content_test.go
Normal file
51
sort-cli/content_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const input = `
|
||||||
|
Zimbabwe
|
||||||
|
Africa
|
||||||
|
America
|
||||||
|
Zimbabwe
|
||||||
|
Africa
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
c := &content{}
|
||||||
|
c.Load(strings.NewReader(input))
|
||||||
|
|
||||||
|
assert.Equal(t, input, c.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnique(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Name string
|
||||||
|
Content string
|
||||||
|
Expected string
|
||||||
|
}{
|
||||||
|
{"Unique",
|
||||||
|
input,
|
||||||
|
`
|
||||||
|
Zimbabwe
|
||||||
|
Africa
|
||||||
|
America`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
|
||||||
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
|
c := &content{}
|
||||||
|
c.Load(strings.NewReader(tc.Content))
|
||||||
|
|
||||||
|
c.Uniques()
|
||||||
|
|
||||||
|
assert.Equal(t, tc.Expected, c.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
34
sort-cli/loader.go
Normal file
34
sort-cli/loader.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func open(sources []string) io.Reader {
|
||||||
|
rs := make([]io.Reader, 0, len(sources))
|
||||||
|
|
||||||
|
for _, source := range sources {
|
||||||
|
var r io.Reader
|
||||||
|
|
||||||
|
if source == "-" {
|
||||||
|
r = os.Stdin
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
log.Fatalf("file not exists: %s", source)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("file open file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r = f
|
||||||
|
}
|
||||||
|
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.MultiReader(rs...)
|
||||||
|
}
|
204
sort-cli/main.go
204
sort-cli/main.go
@ -1,211 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cfg struct {
|
|
||||||
Key int
|
|
||||||
Numeric bool
|
|
||||||
Reverse bool
|
|
||||||
Unique bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Print(run())
|
result := run()
|
||||||
|
|
||||||
|
if _, err := os.Stdout.WriteString(result); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() string {
|
func run() string {
|
||||||
var c Cfg
|
cfg := &Config{}
|
||||||
|
cfg.ParseFlags()
|
||||||
|
|
||||||
flag.IntVar(&c.Key, "k", 0, "sort via column")
|
lines := content{}
|
||||||
flag.BoolVar(&c.Numeric, "n", false, "compare according to string numerical value")
|
lines.Load(open(cfg.Sources))
|
||||||
flag.BoolVar(&c.Reverse, "r", false, "reverse the result of comparisons")
|
|
||||||
flag.BoolVar(&c.Unique, "u", false, "output only the first of an equal run")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
return doSort(&c, flag.Args())
|
|
||||||
}
|
|
||||||
|
|
||||||
func doSort(cfg *Cfg, sources []string) string {
|
|
||||||
r := splitLines(load(sources), []byte("\n"), cfg.Key)
|
|
||||||
|
|
||||||
if cfg.Unique {
|
if cfg.Unique {
|
||||||
r = uniques(r)
|
lines.Uniques()
|
||||||
}
|
}
|
||||||
|
|
||||||
rs := &Rows{s: r, cfg: cfg}
|
lines.Sort(cfg.Reverse)
|
||||||
sort.Sort(rs)
|
|
||||||
|
|
||||||
return rs.String()
|
return lines.String()
|
||||||
}
|
|
||||||
|
|
||||||
type Row struct {
|
|
||||||
Line []byte
|
|
||||||
Column []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rows struct {
|
|
||||||
s []*Row
|
|
||||||
cfg *Cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Rows) Len() int { return len(p.s) }
|
|
||||||
func (p *Rows) Less(i, j int) bool {
|
|
||||||
s := p.s
|
|
||||||
|
|
||||||
lr := s[i]
|
|
||||||
rr := s[j]
|
|
||||||
|
|
||||||
if p.cfg.Reverse {
|
|
||||||
lr, rr = rr, lr
|
|
||||||
}
|
|
||||||
|
|
||||||
var l, r []rune
|
|
||||||
|
|
||||||
if p.cfg.Key == 0 {
|
|
||||||
l, r = bytes.Runes(lr.Line), bytes.Runes(rr.Line)
|
|
||||||
} else {
|
|
||||||
l, r = bytes.Runes(lr.Column), bytes.Runes(rr.Column)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln := len(l)
|
|
||||||
rn := len(r)
|
|
||||||
|
|
||||||
for i := 0; i < min(ln, rn); i++ {
|
|
||||||
if l[i] == r[i] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return l[i] < r[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ln < rn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Rows) Swap(i, j int) {
|
|
||||||
s := p.s
|
|
||||||
|
|
||||||
s[i], s[j] = s[j], s[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Rows) String() string {
|
|
||||||
r := p.s
|
|
||||||
|
|
||||||
var n int
|
|
||||||
for _, s := range r {
|
|
||||||
n += len(s.Line)
|
|
||||||
}
|
|
||||||
n += len(r) * len("\n")
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
sb.Grow(n)
|
|
||||||
|
|
||||||
for _, c := range r {
|
|
||||||
for _, rn := range c.Line {
|
|
||||||
sb.WriteByte(rn)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(sources []string) []byte {
|
|
||||||
if len(sources) == 0 {
|
|
||||||
return loadStdin()
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs := make([][]byte, 0, len(sources))
|
|
||||||
for _, path := range sources {
|
|
||||||
if path == "-" {
|
|
||||||
inputs = append(inputs, loadStdin())
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs = append(inputs, loadFile(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalSize int
|
|
||||||
for _, s := range inputs {
|
|
||||||
totalSize += len(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.Grow(totalSize)
|
|
||||||
|
|
||||||
for _, c := range inputs {
|
|
||||||
b.Write(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadStdin() []byte {
|
|
||||||
b, err := io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("can't read stdin: %e", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadFile(path string) []byte {
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
log.Fatalf("file not exists: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("file open file: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitLines(b, sp []byte, key int) []*Row {
|
|
||||||
r := make([]*Row, 0, bytes.Count(b, sp))
|
|
||||||
for _, b := range bytes.Split(b, sp) {
|
|
||||||
var column []byte
|
|
||||||
|
|
||||||
if key != 0 {
|
|
||||||
bs := bytes.Split(b, []byte(" "))
|
|
||||||
|
|
||||||
if len(bs) < key {
|
|
||||||
continue // TODO is it error or not?
|
|
||||||
// log.Fatalf("Column for key \"%d\" doesn't exists", cfg.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
column = bs[key-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
r = append(r, &Row{Line: b, Column: column})
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniques(r []*Row) []*Row {
|
|
||||||
m := make(map[string]*Row, len(r))
|
|
||||||
|
|
||||||
for _, r := range r {
|
|
||||||
r := r
|
|
||||||
|
|
||||||
m[string(r.Line)] = r
|
|
||||||
}
|
|
||||||
|
|
||||||
return maps.Values(m)
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user