diff --git a/README.md b/README.md index 9c18cfa..47e315e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ USAGE: snip [global options] command [command options] [arguments...] VERSION: - 1.0.2 + 2.0.0 COMMANDS: add, a snip add -k="port" -c="lsof -i :{p}" -desc="List processes listening on a particular port" diff --git a/main.go b/main.go index d799fc7..3f3c98c 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ func Action(fn func(c *cli.Context) error) func(c *cli.Context) error { func main() { app := cli.NewApp() - app.Version = "1.0.2" + app.Version = "2.0.0" app.Usage = "Save snippets: commands, texts, emoji, etc." app.EnableBashCompletion = true app.Commands = []cli.Command{ diff --git a/snippet/snippet.go b/snippet/snippet.go index 7bfc06b..c787ac1 100644 --- a/snippet/snippet.go +++ b/snippet/snippet.go @@ -1,26 +1,18 @@ package snippet import ( - "bufio" - "bytes" + "encoding/csv" "fmt" - "io/ioutil" + "io" "os" "os/user" "path" - "regexp" "strings" "github.com/baopham/snip/util" "github.com/renstrom/fuzzysearch/fuzzy" ) -// EOL end of line -const EOL = "\n" - -// SnippetSeparator between 2 snippets -const SnippetSeparator = ">>>>>>" - const FileMode = 0600 const ( @@ -42,6 +34,8 @@ type Snippet struct { func (s *Snippet) Save(filePath string) error { file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, FileMode) + defer util.Check(file.Close) + if err != nil { return err } @@ -56,54 +50,62 @@ func (s *Snippet) Save(filePath string) error { return SnippetAlreadyExistError{Keyword: existingSnippet.Keyword} } - defer util.Check(file.Close) - - info, err := file.Stat() + w := csv.NewWriter(file) - if err != nil { + if err := w.Write([]string{s.Keyword, s.Content, s.Description}); err != nil { return err } - content := s.String() + w.Flush() - if info.Size() > 0 { - content = EOL + SnippetSeparator + EOL + content - } - - if _, err = file.WriteString(content); err != nil { - return err - } - - return nil + return w.Error() } // Remove a saved snippet func (s *Snippet) Remove(filePath string) error { - b, err := ioutil.ReadFile(filePath) + rows := make([][]string, 0) + + file, err := os.Open(filePath) if err != nil { return err } - content := string(b) + csvr := csv.NewReader(file) - re := regexp.MustCompile(fmt.Sprintf("(?m)(%s)*%s$%s*", SnippetSeparator+EOL, regexp.QuoteMeta(s.String()), EOL)) + for { + row, err := csvr.Read() - newContent := re.ReplaceAllString(content, "") + if err == io.EOF { + break + } + + if err != nil { + return err + } - lines := strings.Split(newContent, EOL) + keyword, content, description := row[0], row[1], row[2] - if lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] + if s.Keyword == keyword { + continue + } + + rows = append(rows, []string{keyword, content, description}) } - if len(lines) > 0 && lines[0] == SnippetSeparator { - lines = lines[1:] + err = file.Close() + + if err != nil { + return err } - newContent = strings.Join(lines, EOL) + file, err = os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, FileMode) + + defer util.Check(file.Close) + + w := csv.NewWriter(file) - return ioutil.WriteFile(filePath, []byte(newContent), FileMode) + return w.WriteAll(rows) } // Get all saved snippets @@ -112,7 +114,7 @@ func GetAll(filePath string) ([]*Snippet, error) { } // SearchExact exact search by keyword -func SearchExact(keyword string, filePath string) (*Snippet, error) { +func SearchExact(keyword, filePath string) (*Snippet, error) { snippets, err := searchSnippets(keyword, filePath, SEARCH_EXACT) if err != nil { @@ -127,14 +129,10 @@ func SearchExact(keyword string, filePath string) (*Snippet, error) { } // Search fuzzy search by given search term -func Search(searchTerm string, filePath string) ([]*Snippet, error) { +func Search(searchTerm, filePath string) ([]*Snippet, error) { return searchSnippets(searchTerm, filePath, SEARCH_FUZZY) } -func (s *Snippet) String() string { - return fmt.Sprintf("%s|%s|%s", s.Keyword, s.Content, s.Description) -} - // Build snippet actual content using the given placeholders func (s *Snippet) Build(placeholders map[string]string) string { content := s.Content @@ -167,7 +165,7 @@ func SnippetFile() (string, error) { return "", err } - filePath := path.Join(dir, "snippets") + filePath := path.Join(dir, "snippets.csv") return filePath, err } @@ -195,47 +193,16 @@ func init() { } } -func getScanner(file *os.File) *bufio.Scanner { - const splitSeparator = EOL + SnippetSeparator + EOL - - trimSeparator := func(data []byte) []byte { - return bytes.Trim(data, splitSeparator) - } - - splitter := func(data []byte, atEOF bool) (advanced int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - if i := strings.Index(string(data), splitSeparator); i >= 0 { - return i + 1, trimSeparator(data[0:i]), nil - } - - if atEOF { - return len(data), trimSeparator(data), nil - } - - return - } - - scanner := bufio.NewScanner(file) - - scanner.Split(splitter) - - return scanner -} - -func searchSnippets(searchTerm string, filePath string, exact SearchCode) ([]*Snippet, error) { +func searchSnippets(searchTerm, filePath string, exact SearchCode) ([]*Snippet, error) { var snippets []*Snippet file, err := os.Open(filePath) + defer util.Check(file.Close) if err != nil { return snippets, err } - scanner := getScanner(file) - matcher := fuzzy.MatchFold if exact == SEARCH_EXACT { @@ -244,19 +211,29 @@ func searchSnippets(searchTerm string, filePath string, exact SearchCode) ([]*Sn matcher = func(k, c string) bool { return true } } - for line := 1; scanner.Scan(); line++ { - lineContent := scanner.Text() + csvr := csv.NewReader(file) - if !matcher(searchTerm, lineContent) { - continue + for { + row, err := csvr.Read() + + if err == io.EOF { + return snippets, nil + } + + if err != nil { + return snippets, err } - record := strings.Split(lineContent, "|") + keyword, content, description := row[0], row[1], row[2] + + if !matcher(searchTerm, keyword) && !matcher(searchTerm, content) && !matcher(searchTerm, description) { + continue + } found := &Snippet{ - Keyword: record[0], - Content: record[1], - Description: record[2], + Keyword: keyword, + Content: content, + Description: description, } snippets = append(snippets, found) @@ -270,7 +247,7 @@ func searchSnippets(searchTerm string, filePath string, exact SearchCode) ([]*Sn } func exactMatcher(source string, target string) bool { - return regexp.MustCompile(fmt.Sprintf(`^%s\|`, source)).MatchString(target) + return strings.TrimSpace(source) == strings.TrimSpace(target) } func panicIfError(e error) { diff --git a/snippet/snippet_test.go b/snippet/snippet_test.go index 6d5db76..a830225 100644 --- a/snippet/snippet_test.go +++ b/snippet/snippet_test.go @@ -11,6 +11,22 @@ import ( "strings" ) +type SnippetContent string + +func (c SnippetContent) mustContain(snippet Snippet) { + content := string(c) + Expect(content).To(ContainSubstring(snippet.Keyword)) + Expect(content).To(ContainSubstring(snippet.Content)) + Expect(content).To(ContainSubstring(snippet.Description)) +} + +func (c SnippetContent) mustNotContain(snippet Snippet) { + content := string(c) + Expect(content).ToNot(ContainSubstring(snippet.Keyword)) + Expect(content).ToNot(ContainSubstring(snippet.Content)) + Expect(content).ToNot(ContainSubstring(snippet.Description)) +} + var _ = Describe("Snippet", func() { var ( fakeFilePath string @@ -31,10 +47,11 @@ var _ = Describe("Snippet", func() { } } - getFileContent := func(fakeFilePath string) string { + getFileContent := func(fakeFilePath string) SnippetContent { b, err := ioutil.ReadFile(fakeFilePath) Expect(err).To(BeNil()) - return string(b) + str := strings.TrimSpace(string(b)) + return SnippetContent(str) } seedSnippet := func() Snippet { @@ -74,12 +91,12 @@ var _ = Describe("Snippet", func() { content := getFileContent(fakeFilePath) - Expect(content).To(ContainSubstring(snippet.String())) + content.mustContain(snippet) }) }) Context("when saving multiple snippets", func() { - It("should save the snippets with correct separator", func() { + It("should save the snippets", func() { snippet1 := seedSnippet() saveSnippet(snippet1, fakeFilePath) @@ -88,10 +105,10 @@ var _ = Describe("Snippet", func() { saveSnippet(snippet2, fakeFilePath) - content := strings.Split(getFileContent(fakeFilePath), EOL+SnippetSeparator+EOL) + content := strings.Split(string(getFileContent(fakeFilePath)), "\n") - Expect(content[0]).To(Equal(snippet1.String())) - Expect(content[1]).To(Equal(snippet2.String())) + SnippetContent(content[0]).mustContain(snippet1) + SnippetContent(content[1]).mustContain(snippet2) }) }) @@ -103,8 +120,7 @@ var _ = Describe("Snippet", func() { content := getFileContent(fakeFilePath) - Expect(content).To(ContainSubstring(snippet.String())) - Expect(content).To(ContainSubstring(snippet.Content)) + content.mustContain(snippet) }) }) @@ -115,8 +131,8 @@ var _ = Describe("Snippet", func() { saveSnippet(snippet, fakeFilePath) found, err := SearchExact(snippet.Keyword, fakeFilePath) - Expect(err).To(BeNil()) + Expect(err).To(BeNil()) Expect(*found).To(Equal(snippet)) }) }) @@ -210,8 +226,6 @@ var _ = Describe("Snippet", func() { }) Context("when calling snippet.Remove()", func() { - separator := EOL + SnippetSeparator + EOL - saveThreeSnippets := func() (Snippet, Snippet, Snippet) { By("saving 3 snippets") @@ -231,7 +245,9 @@ var _ = Describe("Snippet", func() { By("checking that the snippets are indeed saved") - Expect(content).To(Equal(snippet1.String() + separator + snippet2.String() + separator + snippet3.String())) + content.mustContain(snippet1) + content.mustContain(snippet2) + content.mustContain(snippet3) return snippet1, snippet2, snippet3 } @@ -250,7 +266,10 @@ var _ = Describe("Snippet", func() { newContent := getFileContent(fakeFilePath) - Expect(newContent).To(Equal(snippet2.String() + separator + snippet3.String())) + Expect(strings.Split(string(newContent), "\n")).To(HaveLen(2)) + newContent.mustNotContain(snippet1) + newContent.mustContain(snippet2) + newContent.mustContain(snippet3) }) }) @@ -268,7 +287,10 @@ var _ = Describe("Snippet", func() { newContent := getFileContent(fakeFilePath) - Expect(newContent).To(Equal(snippet1.String() + separator + snippet2.String())) + Expect(strings.Split(string(newContent), "\n")).To(HaveLen(2)) + newContent.mustContain(snippet1) + newContent.mustContain(snippet2) + newContent.mustNotContain(snippet3) }) }) @@ -286,7 +308,9 @@ var _ = Describe("Snippet", func() { newContent := getFileContent(fakeFilePath) - Expect(newContent).To(Equal(snippet1.String() + separator + snippet3.String())) + newContent.mustContain(snippet1) + newContent.mustNotContain(snippet2) + newContent.mustContain(snippet3) }) }) })