Skip to content

Commit

Permalink
feat:support remove tag, login by id and refactor db
Browse files Browse the repository at this point in the history
  • Loading branch information
vimiix committed Dec 10, 2023
1 parent 9645909 commit 3832d3a
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 56 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<img src="https://img.shields.io/badge/author-Vimiix-blue" /></a>
</p>

🦅 ssx is an ssh hunter.
🦅 ssx is a retentive ssh client.

It will automatically remember the server which login through it,
so you do not need to enter the password again when you log in again.
Expand All @@ -29,7 +29,7 @@ Download binary from [releases](https://github.com/vimiix/ssx/releases), extract
### Add a new entry

```bash
ssx -s [USER@]HOST[:PORT] [-i IDENTIDY_FILE]
ssx -s [USER@]HOST[:PORT] [-k IDENTITY_FILE]
```

If given address matched an exist entry, ssx will login directly.
Expand Down Expand Up @@ -66,10 +66,29 @@ ssx list
### Tag an entry

```bash
ssx tag -i <ENTRY_ID> -t TAG1 [-t TAG2 ...]
ssx tag -i <ENTRY_ID> [-t TAG1 [-t TAG2 ...]] [-d TAG3 [-d TAG4 ...]]
```
Once we tag the entry, we can log in through the tag later.
### Login
```bash
# login by interacting, just run ssx
ssx

# login by entry id
ssx -i <ID>

# login by address, support partial words
ssx -s <ADDRESS>

# login by tag
ssx -t <TAG>

# If more than one flag of -i, -s ,-t specified,
# priority is ENTRY_ID > ADDRESS > TAG_NAME
```
### Delete an entry
```bash
Expand Down
2 changes: 1 addition & 1 deletion cmd/ssx/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func newDeleteCmd() *cobra.Command {
var ids []int
cmd := &cobra.Command{
Use: "delete",
Short: "delete an entry by ID",
Short: "delete entry by id",
Example: "ssx delete -i1 [-i2 ...]",
RunE: func(cmd *cobra.Command, args []string) error {
if len(ids) == 0 {
Expand Down
11 changes: 5 additions & 6 deletions cmd/ssx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ func NewRoot() *cobra.Command {
opt := &ssx.CmdOption{}
root := &cobra.Command{
Use: "ssx",
Short: "🦅 ssx is an ssh hunter",
Example: `$ ssx
$ ssx -s [USER@]HOST[:PORT] [-i IDENTITY_FILE]
$ ssx -t TAG_NAME
`,
Short: "🦅 ssx is a retentive ssh client",
Example: `# If more than one flag of -i, -s ,-t specified, priority is ENTRY_ID > ADDRESS > TAG_NAME
ssx [-i ENTRY_ID] [-s [USER@]HOST[:PORT]] [-k IDENTITY_FILE] [-t TAG_NAME]`,
SilenceUsage: true,
SilenceErrors: true,
DisableAutoGenTag: true,
Expand All @@ -50,9 +48,10 @@ $ ssx -t TAG_NAME
},
}
root.Flags().StringVarP(&opt.DBFile, "file", "f", "", "filepath to store auth data")
root.Flags().Uint64VarP(&opt.EntryID, "id", "i", 0, "entry id")
root.Flags().StringVarP(&opt.Addr, "server", "s", "", "target server address\nsupport formats: [user@]host[:port]")
root.Flags().StringVarP(&opt.Tag, "tag", "t", "", "search entry by tag")
root.Flags().StringVarP(&opt.IdentityFile, "identity", "i", "", "identity_file path")
root.Flags().StringVarP(&opt.IdentityFile, "keyfile", "k", "", "identity_file path")

root.PersistentFlags().BoolVarP(&printVersion, "version", "v", false, "print ssx version")
root.PersistentFlags().BoolVar(&logVerbose, "verbose", false, "output detail logs")
Expand Down
30 changes: 23 additions & 7 deletions cmd/ssx/cmd/tag.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,41 @@
package cmd

import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

func newTagCmd() *cobra.Command {
var (
tags []string
id int
appendtTags []string
deleteTags []string
id int
)
cmd := &cobra.Command{
Use: "tag",
Short: "tag an entry by id",
Example: "ssx tag -i 1 -t tag1 [-t tag2]",
Short: "add or delete tag for entry by id",
Example: "ssx tag -i <ENTRY_ID> [-t TAG1 [-t TAG2 ...]] [-d TAG3 [-d TAG4 ...]]",
RunE: func(cmd *cobra.Command, args []string) error {
return ssxInst.AppendTagByID(id, tags...)
if len(appendtTags) == 0 && len(deleteTags) == 0 {
return errors.New("no tag is spicified")
}
if len(deleteTags) > 0 {
if err := ssxInst.DeleteTagByID(id, deleteTags...); err != nil {
return err
}
}
if len(appendtTags) > 0 {
if err := ssxInst.AppendTagByID(id, appendtTags...); err != nil {
return err
}
}
return nil
},
}

cmd.Flags().StringSliceVarP(&tags, "tag", "t", nil, "tag name")
cmd.Flags().IntVarP(&id, "id", "i", 0, "entry id")
cmd.MarkFlagsRequiredTogether("id", "tag")
cmd.Flags().StringSliceVarP(&appendtTags, "tag", "t", nil, "tag name to add")
cmd.Flags().StringSliceVarP(&deleteTags, "delete", "d", nil, "tag name to delete")
_ = cmd.MarkFlagRequired("id")
return cmd
}
1 change: 1 addition & 0 deletions internal/errmsg/errmsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ import (
var (
ErrEntryNotExist = errors.New("entry does not exist")
ErrRepoNotOpen = errors.New("repo is not open")
ErrNoEntry = errors.New("no entry found")
)
110 changes: 110 additions & 0 deletions internal/slice/slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package slice

// Distinct returns the unique vals of a slice
func Distinct[T comparable](arrs []T) []T {
m := make(map[T]int)
order := 0
for idx := range arrs {
if _, exist := m[arrs[idx]]; !exist {
m[arrs[idx]] = order
order++
}
}
res := make([]T, len(m))
for k, v := range m {
res[v] = k
}
return res
}

// Union returns a slice that contains the unique values of all the input slices
func Union[T comparable](arrs ...[]T) []T {
m := make(map[T]int)
order := 0
for idx1 := range arrs {
for idx2 := range arrs[idx1] {
if _, exist := m[arrs[idx1][idx2]]; !exist {
m[arrs[idx1][idx2]] = order
order++
}
}
}

ret := make([]T, len(m))
for k, v := range m {
ret[v] = k
}

return ret
}

// Intersect returns a slice of values that are present in all the input slices
func Intersect[T comparable](arrs ...[]T) []T {
m := make(map[T]int)
var order []T
for idx1 := range arrs {
tmpArr := Distinct(arrs[idx1])
for idx2 := range tmpArr {
count, ok := m[tmpArr[idx2]]
if !ok {
order = append(order, tmpArr[idx2])
m[tmpArr[idx2]] = 1
} else {
m[tmpArr[idx2]] = count + 1
}
}
}

var (
ret []T
lenArrs = len(arrs)
)
for idx := range order {
if m[order[idx]] == lenArrs {
ret = append(ret, order[idx])
}
}

return ret
}

// Difference returns a slice of values that are only present in one of the input slices
func Difference[T comparable](arrs ...[]T) []T {
m := make(map[T]int)
var order []T
for idx1 := range arrs {
tmpArr := Distinct(arrs[idx1])
for idx2 := range tmpArr {
count, ok := m[tmpArr[idx2]]
if !ok {
order = append(order, tmpArr[idx2])
m[tmpArr[idx2]] = 1
} else {
m[tmpArr[idx2]] = count + 1
}
}
}

var (
ret []T
)
for idx := range order {
if m[order[idx]] == 1 {
ret = append(ret, order[idx])
}
}

return ret
}

// Delete deletes the element from the slice
func Delete[T comparable](slice []T, elems ...T) []T {
for _, val := range elems {
for idx, elem := range slice {
if val == elem {
slice = append(slice[:idx], slice[idx+1:]...)
}
}
}
return slice
}
108 changes: 108 additions & 0 deletions internal/slice/slice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package slice

import (
"testing"

"github.com/stretchr/testify/assert"
)

type typ struct{ value int }

func (a typ) Compare(b typ) int {
if a.value == b.value {
return 0
}
return 1
}

func TestDistinct(t *testing.T) {
t.Run("string", func(t *testing.T) {
actual := Distinct([]string{"a", "a", "b", "b"})
assert.Equal(t, []string{"a", "b"}, actual)
})

t.Run("integer", func(t *testing.T) {
actual := Distinct([]int{1, 1, 3, 3, 2})
assert.Equal(t, []int{1, 3, 2}, actual)
})

t.Run("object", func(t *testing.T) {
actual := Distinct([]typ{{1}, {1}, {2}})
assert.Equal(t, []typ{{1}, {2}}, actual)
})
}

func TestUnion(t *testing.T) {
t.Run("string", func(t *testing.T) {
actual := Union([]string{"a", "a", "b"}, []string{"b", "c"})
assert.Equal(t, []string{"a", "b", "c"}, actual)
})

t.Run("integer", func(t *testing.T) {
actual := Union([]int{1, 1, 2, 3}, []int{2, 2, 3, 4}, []int{3, 4, 5})
assert.Equal(t, []int{1, 2, 3, 4, 5}, actual)
})

t.Run("integer_order", func(t *testing.T) {
actual := Union([]int{1, 2, 2, 3}, []int{10, 10, 3, 6}, []int{4, 2, 8})
assert.Equal(t, []int{1, 2, 3, 10, 6, 4, 8}, actual)
})

t.Run("object", func(t *testing.T) {
actual := Union([]typ{{1}, {1}, {2}}, []typ{{1}, {3}})
assert.Equal(t, []typ{{1}, {2}, {3}}, actual)
})
}

func TestIntersect(t *testing.T) {
t.Run("string", func(t *testing.T) {
actual := Intersect([]string{"a", "a", "b"}, []string{"b", "c"})
assert.Equal(t, []string{"b"}, actual)
})

t.Run("integer", func(t *testing.T) {
actual := Intersect([]int{1, 1, 3, 2}, []int{2, 10, 3, 4}, []int{2, 3, 4, 5})
assert.Equal(t, []int{3, 2}, actual)
})

t.Run("object", func(t *testing.T) {
actual := Intersect([]typ{{1}, {1}, {2}}, []typ{{1}, {3}})
assert.Equal(t, []typ{{1}}, actual)
})
}

func TestDifference(t *testing.T) {
t.Run("string", func(t *testing.T) {
actual := Difference([]string{"a", "a", "b"}, []string{"b", "c"})
assert.Equal(t, []string{"a", "c"}, actual)
})

t.Run("integer", func(t *testing.T) {
actual := Difference([]int{1, 1, 3, 2}, []int{2, 10, 3, 4}, []int{2, 3, 4, 5})
assert.Equal(t, []int{1, 10, 5}, actual)
})

t.Run("object", func(t *testing.T) {
actual := Difference([]typ{{1}, {1}, {2}}, []typ{{1}, {3}})
assert.Equal(t, []typ{{2}, {3}}, actual)
})
}

func TestDelete(t *testing.T) {
tests := []struct {
name string
slice []int
remove []int
expect []int
}{
{"common", []int{1, 2, 3, 4, 5}, []int{2, 4}, []int{1, 3, 5}},
{"partial-non-exist", []int{1, 2, 3, 4, 5}, []int{2, 6}, []int{1, 3, 4, 5}},
{"all-non-exist", []int{1, 2, 3}, []int{6}, []int{1, 2, 3}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := Delete(tt.slice, tt.remove...)
assert.Equal(t, tt.expect, actual)
})
}
}
Loading

0 comments on commit 3832d3a

Please sign in to comment.