Compare commits
8 Commits
f2976c66e3
...
main
Author | SHA1 | Date | |
---|---|---|---|
021ea61c9b | |||
7f93ae2c20 | |||
6e63af80a6 | |||
8eebdc0d38 | |||
454554f8e0 | |||
c0fe9a4a46 | |||
214bf9acf5 | |||
158e963c7b |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
**/__debug_bin*
|
**/__debug_bin*
|
||||||
main
|
main
|
||||||
|
/pat
|
||||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -13,7 +13,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/cmd/main.go",
|
"program": "${workspaceFolder}/cmd/main.go",
|
||||||
"args": [ ".."]
|
"args": [ "."]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
40
README.md
40
README.md
@ -1,18 +1,22 @@
|
|||||||
# PAT
|
# PAT
|
||||||
|
|
||||||
|
|
||||||
## What it Does
|
## What it Does
|
||||||
|
|
||||||
`pat` is a command-line tool for concatenating and displaying the contents of files and directories. It:
|
`pat` is a command-line tool for concatenating and displaying the contents of files and directories. It:
|
||||||
|
|
||||||
1. Processes the specified files and directories recursively.
|
1. Processes the specified files and directories recursively.
|
||||||
2. Appends content to a structured output with file paths and a delimiter for clarity.
|
2. Appends content to a structured output with file paths and a delimiter for clarity.
|
||||||
3. Copies the resulting output to the system clipboard for convenient sharing or reuse.
|
3. Copies the resulting output to the system clipboard for convenient sharing or reuse.
|
||||||
|
|
||||||
Example use case:
|
Example use case:
|
||||||
|
|
||||||
- Aggregate and view the contents of multiple files or directories in one command.
|
- Aggregate and view the contents of multiple files or directories in one command.
|
||||||
- Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms.
|
- Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms.
|
||||||
|
- Export your codebase to a markdown document.
|
||||||
|
- Export your codebase to a typst document.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
1. **Golang** (only to install / build):
|
1. **Golang** (only to install / build):
|
||||||
- The application requires the Go programming language (`>= 1.18`) to compile and run.
|
- The application requires the Go programming language (`>= 1.18`) to compile and run.
|
||||||
- Dependency: `golang.design/x/clipboard` for clipboard interaction.
|
- Dependency: `golang.design/x/clipboard` for clipboard interaction.
|
||||||
@ -21,31 +25,43 @@ Example use case:
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install go-cat directly using go install:
|
Install pat:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install git.schreifuchs.ch/schreifuchs/pat@latest
|
git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git
|
||||||
|
cd pat
|
||||||
|
go build -o=pat cmd/main.go
|
||||||
|
mv pat $GOPATH/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In one go:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git && cd pat && go build -o=pat cmd/main.go && mv pat $GOPATH/bin/
|
||||||
|
```
|
||||||
|
|
||||||
The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly.
|
The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly.
|
||||||
|
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
Concatenate files and directories:
|
|
||||||
|
### Concatenate files and directories:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./pat file1.txt folder/
|
pat file1.txt folder/
|
||||||
```
|
```
|
||||||
|
|
||||||
Output is printed to the terminal and copied to the clipboard, allowing you to paste it elsewhere.
|
Output is printed to the terminal and copied to the clipboard, allowing you to paste it elsewhere.
|
||||||
|
|
||||||
|
### Create printed
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
pat -t -i .gitignore . | typst compile /dev/stdin pat.pdf
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Notes
|
#### Notes
|
||||||
|
|
||||||
- The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability.
|
- The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability.
|
||||||
- If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal.
|
- If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
16
cmd/main.go
16
cmd/main.go
@ -13,9 +13,11 @@ import (
|
|||||||
const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n"
|
const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched")
|
ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched")
|
||||||
hiddenFiles := flag.Bool("h", false, "show hidden files")
|
hiddenFiles := flag.Bool("h", false, "show hidden files")
|
||||||
|
delemitter := flag.String("d", DELEMITTER, "delemitter to use to split files when not in markdown mode must contain %s for filename")
|
||||||
|
markdown := flag.Bool("m", false, "markdown mode, outputs files in markdown")
|
||||||
|
typst := flag.Bool("t", false, "typst mode, outputs files in typst")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cats, err := cat.Path(flag.Args()...)
|
cats, err := cat.Path(flag.Args()...)
|
||||||
@ -33,11 +35,19 @@ func main() {
|
|||||||
|
|
||||||
cats = cats.Ignored(i)
|
cats = cats.Ignored(i)
|
||||||
}
|
}
|
||||||
if *hiddenFiles == false {
|
|
||||||
|
if !*hiddenFiles {
|
||||||
cats = cats.Ignored(ignore.Filesystem{})
|
cats = cats.Ignored(ignore.Filesystem{})
|
||||||
}
|
}
|
||||||
|
|
||||||
out := cats.ToString(DELEMITTER)
|
var out string
|
||||||
|
if *markdown {
|
||||||
|
out = cats.ToMarkdown()
|
||||||
|
} else if *typst {
|
||||||
|
out = cats.ToTypst()
|
||||||
|
} else {
|
||||||
|
out = cats.ToString(*delemitter)
|
||||||
|
}
|
||||||
fmt.Print(out)
|
fmt.Print(out)
|
||||||
|
|
||||||
if err = clip.Copy(out); err != nil {
|
if err = clip.Copy(out); err != nil {
|
||||||
|
@ -6,10 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cater map[string]string
|
type Cater []entry
|
||||||
|
|
||||||
func Path(paths ...string) (c Cater, err error) {
|
func Path(paths ...string) (c Cater, err error) {
|
||||||
c = make(Cater)
|
c = make(Cater, 0, 10)
|
||||||
var p os.FileInfo
|
var p os.FileInfo
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
@ -17,28 +17,29 @@ func Path(paths ...string) (c Cater, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var e entry
|
||||||
if p.IsDir() {
|
if p.IsDir() {
|
||||||
err = c.dir(path)
|
e, err = c.dir(path)
|
||||||
} else {
|
} else {
|
||||||
err = c.file(path)
|
e, err = c.file(path)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c = append(c, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Cater) Ignored(ignore ignorer) Cater {
|
func (c Cater) Ignored(ignore ignorer) Cater {
|
||||||
cat := make(Cater)
|
cat := make(Cater, 0, len(c))
|
||||||
|
ok := func(e entry) bool {
|
||||||
for name, content := range c {
|
return !ignore.Ignore(e.fqname)
|
||||||
if ignore.Ignore(name) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
cat[name] = content
|
|
||||||
|
for _, entry := range c {
|
||||||
|
cat = append(cat, entry.filter(ok))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cat
|
return cat
|
||||||
@ -47,9 +48,20 @@ func (c Cater) Ignored(ignore ignorer) Cater {
|
|||||||
func (c Cater) ToString(delemiter string) string {
|
func (c Cater) ToString(delemiter string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
for name, content := range c {
|
var entries []entry
|
||||||
sb.WriteString(fmt.Sprintf(delemiter, name))
|
entries = c
|
||||||
sb.WriteString(content)
|
|
||||||
|
for len(entries) > 0 {
|
||||||
|
n := make([]entry, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if len(e.children) > 0 {
|
||||||
|
n = append(n, e.children...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(delemiter, e.fqname))
|
||||||
|
sb.WriteString(e.content)
|
||||||
|
}
|
||||||
|
entries = n
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
|
32
pkg/cat/entry.go
Normal file
32
pkg/cat/entry.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package cat
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
name string
|
||||||
|
fqname string
|
||||||
|
content string
|
||||||
|
children []entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e entry) filter(ok func(e entry) bool) entry {
|
||||||
|
children := make([]entry, 0, len(e.children))
|
||||||
|
for _, entry := range e.children {
|
||||||
|
if !ok(entry) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
children = append(children, entry.filter(ok))
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry{
|
||||||
|
name: e.name,
|
||||||
|
fqname: e.fqname,
|
||||||
|
content: e.content,
|
||||||
|
children: children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e entry) traverse(lvl int, do func(e entry, lvl int)) {
|
||||||
|
do(e, lvl)
|
||||||
|
for _, entry := range e.children {
|
||||||
|
entry.traverse(lvl+1, do)
|
||||||
|
}
|
||||||
|
}
|
@ -1,58 +0,0 @@
|
|||||||
package cat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c Cater) dir(dir string) error {
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
i, err := file.Info()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := path.Join(dir, i.Name())
|
|
||||||
|
|
||||||
if !file.IsDir() {
|
|
||||||
c.file(path)
|
|
||||||
} else {
|
|
||||||
c.dir(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Cater) file(filePath string) error {
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// read file into strings.Builder
|
|
||||||
var sb strings.Builder
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sb.WriteString(line)
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c[filePath] = sb.String()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
39
pkg/cat/markdown.go
Normal file
39
pkg/cat/markdown.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package cat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Cater) ToMarkdown() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
write := func(e entry, lvl int) {
|
||||||
|
for range lvl {
|
||||||
|
sb.WriteString("#")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
|
||||||
|
|
||||||
|
if len(e.content) > 0 {
|
||||||
|
prts := strings.Split(e.name, ".")
|
||||||
|
sb.WriteString(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"```%s\n%s\n```\n\n",
|
||||||
|
|
||||||
|
prts[len(prts)-1],
|
||||||
|
strings.ReplaceAll(
|
||||||
|
e.content,
|
||||||
|
"```",
|
||||||
|
"\\`\\`\\`",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range c {
|
||||||
|
e.traverse(1, write)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
81
pkg/cat/read.go
Normal file
81
pkg/cat/read.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package cat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Cater) dir(dir string) (e entry, err error) {
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.fqname = dir
|
||||||
|
e.name = name(dir)
|
||||||
|
e.children = []entry{}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
i, err := file.Info()
|
||||||
|
if err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
path := path.Join(dir, i.Name())
|
||||||
|
|
||||||
|
var ent entry
|
||||||
|
if !file.IsDir() {
|
||||||
|
ent, err = c.file(path)
|
||||||
|
} else {
|
||||||
|
ent, err = c.dir(path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
e.children = append(e.children, ent)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Cater) file(filePath string) (e entry, err error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
e.fqname = filePath
|
||||||
|
e.name = name(filePath)
|
||||||
|
e.children = []entry{}
|
||||||
|
|
||||||
|
// read file into strings.Builder
|
||||||
|
var sb strings.Builder
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
if !utf8.Valid([]byte(line)) {
|
||||||
|
e.content = "blob\n"
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
sb.WriteString(line)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.content = sb.String()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func name(name string) string {
|
||||||
|
ps := strings.Split(name, "/")
|
||||||
|
return ps[len(ps)-1]
|
||||||
|
}
|
42
pkg/cat/typst.go
Normal file
42
pkg/cat/typst.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package cat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Cater) ToTypst() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
write := func(e entry, lvl int) {
|
||||||
|
for range lvl {
|
||||||
|
sb.WriteString("=")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
|
||||||
|
|
||||||
|
if len(e.content) > 0 {
|
||||||
|
prts := strings.Split(e.name, ".")
|
||||||
|
sb.WriteString(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"```%s\n%s\n```\n\n",
|
||||||
|
|
||||||
|
prts[len(prts)-1],
|
||||||
|
strings.ReplaceAll(
|
||||||
|
e.content,
|
||||||
|
"```",
|
||||||
|
"\\`\\`\\`",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range c {
|
||||||
|
|
||||||
|
sb.WriteString("= Export\n")
|
||||||
|
sb.WriteString("#outline()\n")
|
||||||
|
e.traverse(1, write)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
package ignore
|
package ignore
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type Filesystem struct{}
|
type Filesystem struct{}
|
||||||
|
|
||||||
func (f Filesystem) Ignore(name string) bool {
|
func (f Filesystem) Ignore(name string) bool {
|
||||||
return strings.Contains(name, "/.") || strings.HasPrefix(name, ".")
|
parts := strings.Split(name, "/")
|
||||||
|
return strings.HasPrefix(parts[len(parts)-1], ".")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user