diff --git a/README.md b/README.md index 04c89ad..29658fe 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,14 @@ Run `pet configure` id = "" # GitLab Snippets ID visibility = "private" # public or internal or private auto_sync = false # sync automatically when editing snippets +``` + +## Multi directory and multi file setup + +Directories musst be specified as an array. +All `toml` files will be scraped and found snippets will be added. + +Example1: single directory [GHEGist] base_url = "" # GHE base URL @@ -269,6 +277,26 @@ Run `pet configure` auto_sync = false # sync automatically when editing snippets ``` +$ pet configure +[General] +... + snippetdirs = ["/path/to/some/snippets/"] +... +``` + +Example2: multiple directories + +``` +$ pet configure +[General] +... + snippetdirs = ["/path/to/some/snippets/", "/more/snippets/"] +... +``` + If `snippetfile` setting is omitted, new snippets will be added in a seperate file to the first directory. The generated filename is time based. + +Snippet files in `snippetdirs` will not be added to Gist or GitLab. You've to do version control manually. + ## Selector option Example1: Change layout (bottom up) diff --git a/cmd/edit.go b/cmd/edit.go index 37f59dd..3b5ef70 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,11 +1,14 @@ package cmd import ( + "fmt" "os" "github.com/knqyf263/pet/config" petSync "github.com/knqyf263/pet/sync" + "github.com/pkg/errors" "github.com/spf13/cobra" + "gopkg.in/alessio/shellescape.v1" ) // editCmd represents the edit command @@ -17,9 +20,26 @@ var editCmd = &cobra.Command{ } func edit(cmd *cobra.Command, args []string) (err error) { + flag := config.Flag editor := config.Conf.General.Editor snippetFile := config.Conf.General.SnippetFile + var options []string + if flag.Query != "" { + options = append(options, fmt.Sprintf("--query %s", shellescape.Quote(flag.Query))) + } + + if len(config.Conf.General.SnippetDirs) > 0 { + snippetFile, err = selectFile(options, flag.FilterTag) + if err != nil { + return err + } + } + + if snippetFile == "" { + return errors.New("No sippet file seleted") + } + // file content before editing before := fileContent(snippetFile) @@ -50,4 +70,8 @@ func fileContent(fname string) string { func init() { RootCmd.AddCommand(editCmd) + editCmd.Flags().StringVarP(&config.Flag.Query, "query", "q", "", + `Initial value for query`) + editCmd.Flags().StringVarP(&config.Flag.FilterTag, "tag", "t", "", + `Filter tag`) } diff --git a/cmd/list.go b/cmd/list.go index 779f35f..7b24640 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -43,6 +43,10 @@ func list(cmd *cobra.Command, args []string) error { fmt.Fprintf(color.Output, "%s : %s\n", color.HiGreenString(description), color.HiYellowString(command)) } else { + if config.Flag.Debug { + fmt.Fprintf(color.Output, "%12s %s\n", + color.RedString(" Filename:"), snippet.Filename) + } fmt.Fprintf(color.Output, "%12s %s\n", color.HiGreenString("Description:"), snippet.Description) if strings.Contains(snippet.Command, "\n") { diff --git a/cmd/new.go b/cmd/new.go index debde26..7fac3dc 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -180,6 +180,7 @@ func countSnippetLines() int { } func new(cmd *cobra.Command, args []string) (err error) { + var filename string = "" var command string var description string var tags []string @@ -240,7 +241,12 @@ func new(cmd *cobra.Command, args []string) (err error) { } } + if config.Conf.General.SnippetFile != "" { + filename = config.Conf.General.SnippetFile + } + newSnippet := snippet.SnippetInfo{ + Filename: filename, Description: description, Command: command, Tag: tags, @@ -250,9 +256,8 @@ func new(cmd *cobra.Command, args []string) (err error) { return err } - snippetFile := config.Conf.General.SnippetFile if config.Conf.Gist.AutoSync { - return petSync.AutoSync(snippetFile) + return petSync.AutoSync(filename) } return nil diff --git a/cmd/util.go b/cmd/util.go index 8c21e82..a07c058 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -63,7 +63,6 @@ func filter(options []string, tag string) (commands []string, err error) { snippetTexts[t] = s if config.Flag.Color || config.Conf.General.Color { - t = config.Conf.General.Format t = strings.Replace(format, "$command", command, 1) t = strings.Replace(t, "$description", color.HiRedString(s.Description), 1) t = strings.Replace(t, "$tags", color.HiCyanString(tags), 1) @@ -104,6 +103,60 @@ func filter(options []string, tag string) (commands []string, err error) { return commands, nil } +func selectFile(options []string, tag string) (snippetFile string, err error) { + var snippets snippet.Snippets + if err := snippets.Load(); err != nil { + return snippetFile, fmt.Errorf("load snippet failed: %v", err) + } + + if 0 < len(tag) { + var filteredSnippets snippet.Snippets + for _, snippet := range snippets.Snippets { + for _, t := range snippet.Tag { + if tag == t { + filteredSnippets.Snippets = append(filteredSnippets.Snippets, snippet) + } + } + } + snippets = filteredSnippets + } + + snippetTexts := map[string]snippet.SnippetInfo{} + var text string + for _, s := range snippets.Snippets { + command := s.Command + if strings.ContainsAny(command, "\n") { + command = strings.Replace(command, "\n", "\\n", -1) + } + t := fmt.Sprintf("[%s]: %s", s.Description, command) + + tags := "" + for _, tag := range s.Tag { + tags += fmt.Sprintf(" #%s", tag) + } + t += tags + + snippetTexts[t] = s + text += t + "\n" + } + + var buf bytes.Buffer + selectCmd := fmt.Sprintf("%s %s", + config.Conf.General.SelectCmd, strings.Join(options, " ")) + err = run(selectCmd, strings.NewReader(text), &buf) + if err != nil { + return snippetFile, nil + } + + lines := strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") + + for _, line := range lines { + snippetInfo := snippetTexts[line] + snippetFile = fmt.Sprint(snippetInfo.Filename) + } + return snippetFile, nil +} + // CountLines returns the number of lines in a certain buffer func CountLines(r io.Reader) (int, error) { buf := make([]byte, 32*1024) diff --git a/config/config.go b/config/config.go index 96ce032..cba6a82 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ type Config struct { // GeneralConfig is a struct of general config type GeneralConfig struct { SnippetFile string + SnippetDirs []string Editor string Column int SelectCmd string @@ -88,11 +89,20 @@ func (cfg *Config) Load(file string) error { _, err := os.Stat(file) if err == nil { f, err := os.ReadFile(file) + if err != nil { + return err + } + err = toml.Unmarshal(f, cfg) if err != nil { return err } + var snippetdirs []string cfg.General.SnippetFile = expandPath(cfg.General.SnippetFile) + for _, dir := range cfg.General.SnippetDirs { + snippetdirs = append(snippetdirs, expandPath(dir)) // note the = instead of := + } + cfg.General.SnippetDirs = snippetdirs return nil } diff --git a/go.mod b/go.mod index 6b64d45..cb20ef1 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,10 @@ require ( golang.org/x/crypto v0.17.0 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect +) + +require ( + github.com/kennygrant/sanitize v1.2.4 gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 ) diff --git a/go.sum b/go.sum index fb53945..7197e53 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= +github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= diff --git a/snippet/snippet.go b/snippet/snippet.go index e0c91b4..32cf2e4 100644 --- a/snippet/snippet.go +++ b/snippet/snippet.go @@ -5,7 +5,9 @@ import ( "fmt" "os" "sort" + "strings" + "github.com/kennygrant/sanitize" "github.com/knqyf263/pet/config" "github.com/pelletier/go-toml" ) @@ -15,6 +17,7 @@ type Snippets struct { } type SnippetInfo struct { + Filename string Description string Command string `toml:"command,multiline"` Tag []string @@ -23,18 +26,51 @@ type SnippetInfo struct { // Load reads toml file. func (snippets *Snippets) Load() error { + var snippetFiles []string + snippetFile := config.Conf.General.SnippetFile - if _, err := os.Stat(snippetFile); os.IsNotExist(err) { - return nil + if snippetFile != "" { + if _, err := os.Stat(snippetFile); err == nil { + snippetFiles = append(snippetFiles, snippetFile) + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to load snippet file. %v", err) + } else { + return fmt.Errorf( + `snippet file not found. %s +Please run 'pet configure' and provide a correct file path, or remove this +if you only want to provide snippetdirs instead`, + snippetFile, + ) + } } - f, err := os.ReadFile(snippetFile) - if err != nil { - return fmt.Errorf("failed to load snippet file. %v", err) + + for _, dir := range config.Conf.General.SnippetDirs { + if _, err := os.Stat(dir); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("snippet directory not found. %s", dir) + } + return fmt.Errorf("failed to load snippet directory. %v", err) + } + snippetFiles = append(snippetFiles, getFiles(dir)...) } - err = toml.Unmarshal(f, snippets) - if err != nil { - return fmt.Errorf("failed to parse snippet file. %v", err) + // Read files and load snippets + for _, file := range snippetFiles { + tmp := Snippets{} + f, err := os.ReadFile(file) + if err != nil { + return fmt.Errorf("failed to load snippet file. %v", err) + } + + err = toml.Unmarshal(f, &tmp) + if err != nil { + return fmt.Errorf("failed to parse snippet file. %v", err) + } + + for _, snippet := range tmp.Snippets { + snippet.Filename = file + snippets.Snippets = append(snippets.Snippets, snippet) + } } snippets.Order() @@ -43,11 +79,22 @@ func (snippets *Snippets) Load() error { // Save saves the snippets to toml file. func (snippets *Snippets) Save() error { - snippetFile := config.Conf.General.SnippetFile + var snippetFile string + var newSnippets Snippets + for _, snippet := range snippets.Snippets { + if snippet.Filename == "" { + snippetFile = config.Conf.General.SnippetDirs[0] + fmt.Sprintf("%s.toml", strings.ToLower(sanitize.BaseName(snippet.Description))) + newSnippets.Snippets = append(newSnippets.Snippets, snippet) + } else if snippet.Filename == config.Conf.General.SnippetFile { + snippetFile = config.Conf.General.SnippetFile + newSnippets.Snippets = append(newSnippets.Snippets, snippet) + } + } f, err := os.Create(snippetFile) if err != nil { return fmt.Errorf("failed to save snippet file. err: %s", err) } + defer f.Close() return toml.NewEncoder(f).Encode(snippets) } diff --git a/snippet/util.go b/snippet/util.go new file mode 100644 index 0000000..099685f --- /dev/null +++ b/snippet/util.go @@ -0,0 +1,28 @@ +package snippet + +import ( + "log" + "os" + "path/filepath" + "regexp" +) + +func getFiles(path string) (fileList []string) { + tomlRegEx, err := regexp.Compile("^.+\\.(toml)$") + if err != nil { + log.Fatal(err) + } + + err = filepath.Walk(path, func(path string, f os.FileInfo, err error) error { + if err == nil && tomlRegEx.MatchString(f.Name()) { + fileList = append(fileList, path) + } + return nil + }) + + if err != nil { + panic(err) + } + + return fileList +}