Skip to content

Commit 28bbc8f

Browse files
authored
Speed improvements on parsing usage and help (#10)
Iterative line by line parsing with early exit to extract usage and help information.
1 parent f3c300f commit 28bbc8f

File tree

5 files changed

+84
-41
lines changed

5 files changed

+84
-41
lines changed

cmd/alias.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package cmd
66
import (
77
"bytes"
88
"embed"
9-
"log"
109
"os"
1110
"text/template"
1211

@@ -56,7 +55,7 @@ Read the template script 'tome-wrapper.sh.tmpl' for more information on how the
5655
t, err := template.ParseFS(content, "embeds/tome-wrapper.sh.tmpl")
5756
// Capture any error
5857
if err != nil {
59-
log.Fatalln(err)
58+
log.Fatal(err)
6059
}
6160
buf := new(bytes.Buffer)
6261
v, err := cmd.Flags().GetString("output")

cmd/docs.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ Copyright © 2024 Zander Hill <zander@xargs.io>
44
package cmd
55

66
import (
7-
"log"
8-
97
"github.com/spf13/cobra"
108
"github.com/spf13/cobra/doc"
119
)

cmd/lib.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7-
"log"
87
"net/url"
98
"os"
109
"os/exec"

cmd/root.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/gobeam/stringy"
1212
"github.com/spf13/cobra"
1313
"github.com/spf13/viper"
14+
"go.uber.org/zap"
1415
)
1516

1617
var rootDir string
@@ -64,9 +65,11 @@ func init() {
6465
viper.SetDefault("license", "mit")
6566
}
6667

68+
var log *zap.SugaredLogger
69+
6770
// initConfig reads in config file and ENV variables if set.
6871
func initConfig() {
69-
log := createLogger("initConfig", rootCmd.OutOrStderr())
72+
log = createLogger("initConfig", rootCmd.OutOrStderr())
7073
v := viper.GetViper()
7174
var err error
7275
rootDir, err = filepath.Abs(rootDir)

cmd/struct.go

Lines changed: 79 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"bufio"
45
"fmt"
56
"os"
67
"path/filepath"
@@ -48,60 +49,103 @@ func (s *Script) IsExecutable() bool {
4849
return isExecutableByOwner(fileInfo.Mode())
4950
}
5051

51-
func (s *Script) parse() error {
52-
b, err := os.ReadFile(s.path)
52+
// ParseV2 returns the usage and help text for the script
53+
// function aims to return early and perform as little work as possible
54+
// to avoid reading the entire file and stay performant
55+
// with large script folders and files
56+
func (s *Script) ParseV2() (string, string, error) {
57+
log.Debugw("Parsing script", "path", s.path)
58+
file, err := os.Open(s.path)
5359
if err != nil {
54-
return err
60+
return "", "", err
5561
}
56-
57-
if strings.Contains(string(b), UsageKey) || strings.Contains(string(b), LegacyUsageKey) {
58-
lines := strings.Split(string(b), "\n")
59-
var linesStart int
60-
for idx, line := range lines {
61-
if strings.Contains(line, UsageKey) || strings.Contains(line, LegacyUsageKey) {
62-
linesStart = idx
62+
defer file.Close()
63+
64+
scanner := bufio.NewScanner(file)
65+
66+
// Parse the script file for the usage and help text
67+
// Expected structure is:
68+
// #!/bin/bash
69+
// # USAGE: script.sh [options] <arg1> <arg2>
70+
// # This is the help text for the script
71+
// # It can span multiple lines
72+
//
73+
// echo 1
74+
75+
var usage, help string
76+
idx := 0
77+
78+
var helpArr []string
79+
80+
startsWithComment := regexp.MustCompile(`^[/*\-#]+`)
81+
for scanner.Scan() {
82+
t := scanner.Text()
83+
log.Debugw("Parsing line", "line", t)
84+
// Skip the shebang line
85+
if idx == 0 && strings.HasPrefix(t, "#!") {
86+
log.Debugw("shebang", "line", t)
87+
idx++
88+
continue
89+
}
90+
// Normally this is the usage line
91+
if idx == 1 {
92+
log.Debugw("likely usage", "line", t)
93+
if !startsWithComment.MatchString(t) {
94+
usage = ""
95+
help = ""
6396
break
97+
} else {
98+
withoutCommentChars := strings.TrimLeft(t, "#/-*")
99+
regexes := []regexp.Regexp{
100+
*regexp.MustCompile(`(USAGE|SUMMARY):`),
101+
*regexp.MustCompile(fmt.Sprintf(`(%s|%s)`, regexp.QuoteMeta(`$0`), regexp.QuoteMeta(filepath.Base(s.path)))),
102+
*regexp.MustCompile(`TOME_[A-Z_]+`), // ignore tome option flags
103+
}
104+
for _, r := range regexes {
105+
withoutCommentChars = r.ReplaceAllLiteralString(withoutCommentChars, "")
106+
}
107+
usage = strings.TrimSpace(withoutCommentChars)
108+
log.Debugw("usage", "usage", usage)
109+
idx++
64110
}
65111
}
66112

67-
var helpEnds int
68-
for idx, line := range lines[linesStart:] {
69-
if line == "" {
70-
helpEnds = idx + linesStart
71-
break
72-
}
113+
// Scan until we find an empty line
114+
if startsWithComment.MatchString(t) {
115+
t2 := strings.TrimSpace(strings.TrimLeft(t, "#/-*"))
116+
log.Debugw("help line", "line", t2)
117+
helpArr = append(helpArr, t2)
118+
idx++
119+
continue
120+
} else {
121+
break
73122
}
74-
helpTextLines := lines[linesStart:helpEnds]
75-
helpText := strings.Join(helpTextLines, "\n")
123+
}
124+
help = strings.Join(helpArr, "\n")
76125

77-
s.usage = strings.TrimSpace(strings.SplitN(lines[linesStart], ":", 2)[1])
78-
s.help = helpText
126+
return usage, help, nil
127+
}
128+
129+
func (s *Script) parse() error {
130+
usage, help, err := s.ParseV2()
131+
if err != nil {
132+
return err
79133
}
134+
s.usage = usage
135+
s.help = help
136+
80137
return nil
81138
}
82139

83140
// Usage returns the usage string for the script
84141
// after stripping out the script name or $0
85142
// this is done to reduce visual noise
86143
func (s *Script) Usage() string {
87-
baseUsage := s.usage
88-
prefixes := []string{"$0", filepath.Base(s.path)}
89-
for _, prefix := range prefixes {
90-
baseUsage = strings.TrimPrefix(baseUsage, prefix)
91-
}
92-
baseUsage = strings.TrimSpace(baseUsage)
93-
return dedent.Dedent(baseUsage)
144+
return dedent.Dedent(s.usage)
94145
}
95146

96147
func (s *Script) Help() string {
97-
lines := strings.Split(s.help, "\n")
98-
var helpTextLines []string
99-
toTrim := []string{"#", "//", "/\\*", "\\*/", "--"}
100-
toTrimRegex := regexp.MustCompile(fmt.Sprintf("^(%s)+", strings.Join(toTrim, "|")))
101-
for _, line := range lines {
102-
helpTextLines = append(helpTextLines, toTrimRegex.ReplaceAllString(line, ""))
103-
}
104-
return dedent.Dedent(strings.Join(helpTextLines, "\n"))
148+
return dedent.Dedent(s.help)
105149
}
106150

107151
func (s *Script) PathWithoutRoot() string {

0 commit comments

Comments
 (0)