add del command

add tests for all commands

wrap entry point with fang

add --version flag
target to Taskfile
This commit is contained in:
onyx-and-iris 2026-03-29 21:15:56 +01:00
parent 8f6a5d4472
commit 8059255d52
16 changed files with 520 additions and 80 deletions

22
.gitignore vendored
View File

@ -1,6 +1,6 @@
# Auto-generated .gitignore by gignore: github.com/onyx-and-iris/gignore
# Generated by ignr: github.com/onyx-and-iris/ignr
### Go ###
## Go ##
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
@ -10,18 +10,30 @@
*.dll
*.so
*.dylib
bin/
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# End of gignore: github.com/onyx-and-iris/gignore
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
# End of ignr
testdata/

View File

@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"io"
"slices"
"github.com/spf13/cobra"
)
@ -15,27 +16,37 @@ var addCmd = &cobra.Command{
This is useful for excluding files or directories from version control without modifying the .gitignore file.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
f, ok := FileFromContext(cmd.Context())
ctx, ok := ContextObjectFromContext(cmd.Context())
if !ok {
return fmt.Errorf("no exclude file found in context")
}
return runAddCommand(f, args)
return runAddCommand(ctx.Out, ctx.File, args)
},
}
func init() {
rootCmd.AddCommand(addCmd)
RootCmd.AddCommand(addCmd)
}
func runAddCommand(f io.Writer, args []string) error {
for _, pattern := range args {
if _, err := fmt.Fprintln(f, pattern); err != nil {
return fmt.Errorf("error writing to exclude file: %w", err)
}
// runAddCommand adds the specified patterns to the exclude file, ensuring no duplicates
// It handles both file and in-memory buffer cases for testing
func runAddCommand(out io.Writer, f io.ReadWriter, args []string) error {
existingPatterns, err := readExistingPatterns(f)
if err != nil {
return fmt.Errorf("error reading existing patterns: %v", err)
}
fmt.Println("Patterns added to .git/info/exclude file:")
for _, pattern := range args {
fmt.Println(" -", pattern)
if slices.Contains(existingPatterns, pattern) {
fmt.Fprintf(out, "Pattern '%s' already exists in the exclude file. Skipping.\n", pattern)
continue
}
_, err := fmt.Fprintln(f, pattern)
if err != nil {
return fmt.Errorf("error writing to exclude file: %v", err)
}
fmt.Fprintf(out, "Added pattern '%s' to the exclude file.\n", pattern)
}
return nil
}

50
cmd/add_test.go Normal file
View File

@ -0,0 +1,50 @@
package cmd
import (
"bytes"
"testing"
)
func TestRunAddCommand(t *testing.T) {
tests := []struct {
name string
existing string
args []string
expectedOutput string
}{
{
name: "Add new patterns",
existing: "",
args: []string{"*.log", "temp/"},
expectedOutput: "Added pattern '*.log' to the exclude file.\nAdded pattern 'temp/' to the exclude file.\n",
},
{
name: "Add duplicate patterns",
existing: "*.log\ntemp/\n",
args: []string{"*.log", "temp/"},
expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nPattern 'temp/' already exists in the exclude file. Skipping.\n",
},
{
name: "Add mix of new and duplicate patterns",
existing: "*.log\n",
args: []string{"*.log", "temp/"},
expectedOutput: "Pattern '*.log' already exists in the exclude file. Skipping.\nAdded pattern 'temp/' to the exclude file.\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
f := bytes.NewBufferString(tt.existing)
err := runAddCommand(&buf, f, tt.args)
if err != nil {
t.Fatalf("runAddCommand returned an error: %v", err)
}
if buf.String() != tt.expectedOutput {
t.Errorf("Expected output:\n%s\nGot:\n%s", tt.expectedOutput, buf.String())
}
})
}
}

View File

@ -2,20 +2,27 @@ package cmd
import (
"context"
"io"
"os"
)
type contextKey string
const fileContextKey contextKey = "excludeFile"
const contextObjectKey = contextKey("contextObject")
// ContextWithFile returns a new context with the given file set.
func ContextWithFile(ctx context.Context, file *os.File) context.Context {
return context.WithValue(ctx, fileContextKey, file)
type contextObject struct {
File *os.File
Out io.Writer
}
// FileFromContext retrieves the file from the context, if it exists.
func FileFromContext(ctx context.Context) (*os.File, bool) {
file, ok := ctx.Value(fileContextKey).(*os.File)
return file, ok
func createContext(file *os.File, out io.Writer) context.Context {
return context.WithValue(context.Background(), contextObjectKey, &contextObject{
File: file,
Out: out,
})
}
func ContextObjectFromContext(ctx context.Context) (*contextObject, bool) {
obj, ok := ctx.Value(contextObjectKey).(*contextObject)
return obj, ok
}

86
cmd/del.go Normal file
View File

@ -0,0 +1,86 @@
package cmd
import (
"fmt"
"io"
"slices"
"github.com/spf13/cobra"
)
// delCmd represents the del command
var delCmd = &cobra.Command{
Use: "del",
Short: "Delete a pattern from the exclude file",
Long: `The del command removes a specified pattern from the .git/info/exclude file.
This is useful for un-excluding files or directories that were previously excluded.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, ok := ContextObjectFromContext(cmd.Context())
if !ok {
return fmt.Errorf("no exclude file found in context")
}
if len(args) == 0 {
return fmt.Errorf("no pattern provided to delete")
}
pattern := args[0]
return runDelCommand(ctx.Out, ctx.File, pattern)
},
}
func init() {
RootCmd.AddCommand(delCmd)
}
// runDelCommand deletes the specified pattern from the exclude file and writes the updated content back
// It handles both file and in-memory buffer cases for testing
func runDelCommand(out io.Writer, f any, pattern string) error {
r, ok := f.(io.Reader)
if !ok {
return fmt.Errorf("provided file does not support Reader")
}
existingPatterns, err := readExistingPatterns(r)
if err != nil {
return fmt.Errorf("error reading existing patterns: %v", err)
}
if !slices.Contains(existingPatterns, pattern) {
fmt.Fprintf(out, "Pattern '%s' not found in the exclude file. Nothing to delete.\n", pattern)
return nil
}
var updatedPatterns []string
for _, p := range existingPatterns {
if p != pattern {
updatedPatterns = append(updatedPatterns, p)
}
}
var w io.Writer
if t, ok := f.(truncater); ok {
if err := t.Truncate(0); err != nil {
return fmt.Errorf("error truncating exclude file: %w", err)
}
if s, ok := f.(io.Seeker); ok {
if _, err := s.Seek(0, 0); err != nil {
return fmt.Errorf("error seeking to the beginning of exclude file: %w", err)
}
}
w, _ = f.(io.Writer)
} else if buf, ok := f.(interface{ Reset() }); ok {
buf.Reset()
w, _ = f.(io.Writer)
} else {
return fmt.Errorf("provided file does not support writing")
}
if err := writeDefaultExcludeContent(w); err != nil {
return fmt.Errorf("error writing default exclude content: %w", err)
}
for _, p := range updatedPatterns {
if _, err := fmt.Fprintln(w, p); err != nil {
return fmt.Errorf("error writing updated patterns to exclude file: %v", err)
}
}
fmt.Fprintf(out, "Deleted pattern '%s' from the exclude file.\n", pattern)
return nil
}

49
cmd/del_test.go Normal file
View File

@ -0,0 +1,49 @@
package cmd
import (
"bytes"
"testing"
)
func TestRunDelCommand(t *testing.T) {
tests := []struct {
name string
initialContent string
patternToDelete string
expectedOutput string
expectedContent string
}{
{
name: "Delete existing pattern",
initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n",
patternToDelete: "node_modules",
expectedOutput: defaultExcludeFileContent + ".DS_Store\n" + "Deleted pattern 'node_modules' from the exclude file.\n",
},
{
name: "Delete non-existing pattern",
initialContent: defaultExcludeFileContent + "node_modules\n.DS_Store\n",
patternToDelete: "dist",
expectedOutput: "Pattern 'dist' not found in the exclude file. Nothing to delete.\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
buf.WriteString(tt.initialContent)
err := runDelCommand(&buf, &buf, tt.patternToDelete)
if err != nil {
t.Fatalf("runDelCommand returned an error: %v", err)
}
if buf.String() != tt.expectedOutput {
t.Errorf(
"Expected output and content:\n%s\nGot:\n%s",
tt.expectedOutput,
buf.String(),
)
}
})
}
}

View File

@ -18,31 +18,36 @@ that are not empty and do not start with a comment (#).
This is useful for reviewing which files or directories are currently excluded from version control.`,
Args: cobra.NoArgs, // No arguments expected
RunE: func(cmd *cobra.Command, args []string) error {
f, ok := FileFromContext(cmd.Context())
ctx, ok := ContextObjectFromContext(cmd.Context())
if !ok {
return fmt.Errorf("no exclude file found in context")
}
return runListCommand(f, args)
return runListCommand(ctx.Out, ctx.File)
},
}
func init() {
rootCmd.AddCommand(listCmd)
RootCmd.AddCommand(listCmd)
}
// runListCommand is the function that will be executed when the list command is called
func runListCommand(f io.Reader, _ []string) error {
// Read from the exclude file line by line
func runListCommand(out io.Writer, f io.Reader) error {
var count int
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line != "" && !strings.HasPrefix(line, "#") {
fmt.Println(line) // Print each non-empty, non-comment line
fmt.Fprintln(out, line)
count++
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading exclude file: %w", err)
}
if count == 0 {
fmt.Fprintln(out, "No patterns found in the exclude file.")
}
return nil
}

46
cmd/list_test.go Normal file
View File

@ -0,0 +1,46 @@
package cmd
import (
"bytes"
"testing"
)
func TestRunListCommand(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "Empty file",
input: defaultExcludeFileContent,
expected: "No patterns found in the exclude file.\n",
},
{
name: "Exclude file with patterns",
input: defaultExcludeFileContent + "node_modules/\nbuild/\n# This is a comment\n",
expected: "node_modules/\nbuild/\n",
},
{
name: "Exclude file with only comments and empty lines",
input: defaultExcludeFileContent + "# Comment 1\n# Comment 2\n\n",
expected: "No patterns found in the exclude file.\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := bytes.NewBufferString(tt.input)
var output bytes.Buffer
err := runListCommand(&output, reader)
if err != nil {
t.Fatalf("runListCommand returned an error: %v", err)
}
if output.String() != tt.expected {
t.Errorf("Expected output:\n%q\nGot:\n%q", tt.expected, output.String())
}
})
}
}

View File

@ -3,7 +3,7 @@ package cmd
import (
_ "embed"
"fmt"
"os"
"io"
"github.com/spf13/cobra"
)
@ -15,36 +15,42 @@ var resetCmd = &cobra.Command{
Long: `The reset command clears all patterns from the .git/info/exclude file.
This is useful for starting fresh or removing all exclusions at once.`,
RunE: func(cmd *cobra.Command, _ []string) error {
f, ok := FileFromContext(cmd.Context())
ctx, ok := ContextObjectFromContext(cmd.Context())
if !ok {
return fmt.Errorf("no exclude file found in context")
}
return runResetCommand(f)
return resetAndWriteExcludeFile(ctx.File)
},
}
func init() {
rootCmd.AddCommand(resetCmd)
RootCmd.AddCommand(resetCmd)
}
//go:embed template/exclude
var defaultExcludeFile string
// Truncate and seek to beginning
type truncater interface{ Truncate(size int64) error }
// runResetCommand clears the exclude file
func runResetCommand(f *os.File) error {
// Clear the exclude file by truncating it
if err := f.Truncate(0); err != nil {
// resetAndWriteExcludeFile truncates and resets the file, then writes the default content
func resetAndWriteExcludeFile(f any) error {
// Try to assert to io.ReadWriteSeeker for file operations
rws, ok := f.(io.ReadWriteSeeker)
if !ok {
// If not a file, try as io.Writer (for test buffers)
if w, ok := f.(io.Writer); ok {
return writeDefaultExcludeContent(w)
}
return fmt.Errorf("provided file does not support ReadWriteSeeker or Writer")
}
t, ok := f.(truncater)
if !ok {
return fmt.Errorf("provided file does not support Truncate")
}
if err := t.Truncate(0); err != nil {
return fmt.Errorf("error truncating exclude file: %w", err)
}
// Reset the file pointer to the beginning
if _, err := f.Seek(0, 0); err != nil {
if _, err := rws.Seek(0, 0); err != nil {
return fmt.Errorf("error seeking to the beginning of exclude file: %w", err)
}
// Write the default exclude patterns to the file
if _, err := f.WriteString(defaultExcludeFile); err != nil {
return fmt.Errorf("error writing default exclude file: %w", err)
}
return nil
return writeDefaultExcludeContent(rws)
}

28
cmd/reset_test.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
import (
"bytes"
"io"
"testing"
)
func TestRunResetCommand(t *testing.T) {
var buf bytes.Buffer
if err := resetAndWriteExcludeFile(&buf); err != nil {
t.Fatalf("resetAndWriteExcludeFile failed: %v", err)
}
resetContent, err := io.ReadAll(&buf)
if err != nil {
t.Fatalf("failed to read from temp file: %v", err)
}
if string(resetContent) != defaultExcludeFileContent {
t.Errorf(
"unexpected content after reset:\nGot:\n%s\nExpected:\n%s",
string(resetContent),
defaultExcludeFileContent,
)
}
}

View File

@ -1,56 +1,58 @@
package cmd
import (
"context"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
const defaultExcludeFileContent = `# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
`
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "exclude",
Short: "A command line tool to manage .git/info/exclude files",
Long: `Exclude is a command line tool designed to help you manage your .git/info/exclude files.
It allows you to add, list, and remove patterns from the exclude file easily.
It allows you to add, list, and delete patterns from the exclude file easily.
This tool is particularly useful for developers who want to keep their repository clean
by excluding certain files or directories from version control.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// This function runs before any command is executed.
// You can use it to set up global configurations or checks.
// For example, you might want to check if the .git directory exists
if _, err := os.Stat(".git"); os.IsNotExist(err) {
cmd.Println("Error: This command must be run in a Git repository.")
os.Exit(1)
}
f, err := os.OpenFile(".git/info/exclude", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
path, err := cmd.Flags().GetString("path")
if err != nil {
cmd.Println("Error getting path flag:", err)
os.Exit(1)
}
f, err := os.OpenFile(filepath.Join(path, "exclude"), os.O_RDWR|os.O_APPEND, 0644)
if err != nil {
cmd.Println("Error opening .git/info/exclude file:", err)
os.Exit(1)
}
ctx := context.WithValue(context.Background(), fileContextKey, f)
ctx := createContext(f, cmd.OutOrStdout())
cmd.SetContext(ctx)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if f, ok := FileFromContext(cmd.Context()); ok {
defer f.Close()
if obj, ok := ContextObjectFromContext(cmd.Context()); ok {
defer obj.File.Close()
}
},
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
RootCmd.PersistentFlags().
StringP("path", "p", ".git/info/", "Path the exclude file resides in (default is .git/info/)")
}

View File

@ -1,6 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

32
cmd/util.go Normal file
View File

@ -0,0 +1,32 @@
package cmd
import (
"bufio"
"fmt"
"io"
"strings"
)
// readExistingPatterns reads the existing patterns from the exclude file, ignoring comments and empty lines
func readExistingPatterns(f io.Reader) ([]string, error) {
var patterns []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if line != "" && !strings.HasPrefix(line, "#") {
patterns = append(patterns, line)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning exclude file: %v", err)
}
return patterns, nil
}
// writeDefaultExcludeContent writes the default exclude content to the writer
func writeDefaultExcludeContent(w io.Writer) error {
if _, err := w.Write([]byte(defaultExcludeFileContent)); err != nil {
return fmt.Errorf("error writing default exclude file: %w", err)
}
return nil
}

28
go.mod
View File

@ -2,9 +2,35 @@ module github.com/onyx-and-iris/exclude
go 1.24.3
require github.com/spf13/cobra v1.10.2
require (
github.com/charmbracelet/fang v1.0.0
github.com/spf13/cobra v1.10.2
)
require (
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
github.com/charmbracelet/x/ansi v0.11.0 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.4.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

62
go.sum
View File

@ -1,11 +1,73 @@
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0=
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

28
main.go
View File

@ -1,7 +1,31 @@
package main
import "github.com/onyx-and-iris/exclude/cmd"
import (
"context"
"os"
"runtime/debug"
"strings"
"github.com/charmbracelet/fang"
"github.com/onyx-and-iris/exclude/cmd"
)
var version string // Version of the CLI, set during build time
func main() {
cmd.Execute()
if err := fang.Execute(context.Background(), cmd.RootCmd, fang.WithVersion(versionFromBuild())); err != nil {
os.Exit(1)
}
}
func versionFromBuild() string {
if version != "" {
return version
}
info, ok := debug.ReadBuildInfo()
if !ok {
return "(unable to read version)"
}
return strings.Split(info.Main.Version, "-")[0]
}