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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"golang.org/x/exp/maps"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cfg struct {
|
||||
Key int
|
||||
Numeric bool
|
||||
Reverse bool
|
||||
Unique bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Print(run())
|
||||
result := run()
|
||||
|
||||
if _, err := os.Stdout.WriteString(result); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func run() string {
|
||||
var c Cfg
|
||||
cfg := &Config{}
|
||||
cfg.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()
|
||||
|
||||
return doSort(&c, flag.Args())
|
||||
}
|
||||
|
||||
func doSort(cfg *Cfg, sources []string) string {
|
||||
r := splitLines(load(sources), []byte("\n"), cfg.Key)
|
||||
lines := content{}
|
||||
lines.Load(open(cfg.Sources))
|
||||
|
||||
if cfg.Unique {
|
||||
r = uniques(r)
|
||||
lines.Uniques()
|
||||
}
|
||||
|
||||
rs := &Rows{s: r, cfg: cfg}
|
||||
sort.Sort(rs)
|
||||
lines.Sort(cfg.Reverse)
|
||||
|
||||
return rs.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)
|
||||
return lines.String()
|
||||
}
|
||||
|
Reference in New Issue
Block a user