7 Commits
v1.0.0 ... main

Author SHA1 Message Date
021ea61c9b remove binary 2025-05-16 10:25:29 +02:00
7f93ae2c20 blob detection 2025-05-16 10:21:28 +02:00
6e63af80a6 better suffix check 2025-05-07 14:30:59 +02:00
8eebdc0d38 ignore ttf 2025-05-07 11:30:01 +02:00
454554f8e0 add typst export 2025-05-07 11:00:32 +02:00
c0fe9a4a46 fix heading problem 2025-05-02 13:21:27 +02:00
214bf9acf5 added markdown support 2025-05-02 13:18:47 +02:00
11 changed files with 269 additions and 91 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
**/__debug_bin*
main
/pat

2
.vscode/launch.json vendored
View File

@ -13,7 +13,7 @@
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/main.go",
"args": [ ".."]
"args": [ "."]
}
]
}

View File

@ -1,18 +1,22 @@
# PAT
## What it Does
`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.
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.
Example use case:
- 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.
- Export your codebase to a markdown document.
- Export your codebase to a typst document.
## Dependencies
1. **Golang** (only to install / build):
- The application requires the Go programming language (`>= 1.18`) to compile and run.
- Dependency: `golang.design/x/clipboard` for clipboard interaction.
@ -21,31 +25,43 @@ Example use case:
## Installation
Install go-cat directly using go install:
Install pat:
``` sh
go install git.schreifuchs.ch/schreifuchs/pat@latest
```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/
```
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.
## Example Usage
Concatenate files and directories:
### Concatenate files and directories:
```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.
### Create printed
```sh
pat -t -i .gitignore . | typst compile /dev/stdin pat.pdf
```
---
#### Notes
- 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.

View File

@ -13,9 +13,11 @@ import (
const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n"
func main() {
ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched")
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()
cats, err := cat.Path(flag.Args()...)
@ -33,11 +35,19 @@ func main() {
cats = cats.Ignored(i)
}
if *hiddenFiles == false {
if !*hiddenFiles {
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)
if err = clip.Copy(out); err != nil {

View File

@ -6,10 +6,10 @@ import (
"strings"
)
type Cater map[string]string
type Cater []entry
func Path(paths ...string) (c Cater, err error) {
c = make(Cater)
c = make(Cater, 0, 10)
var p os.FileInfo
for _, path := range paths {
@ -17,28 +17,29 @@ func Path(paths ...string) (c Cater, err error) {
if err != nil {
return
}
var e entry
if p.IsDir() {
err = c.dir(path)
e, err = c.dir(path)
} else {
err = c.file(path)
e, err = c.file(path)
}
if err != nil {
return
}
c = append(c, e)
}
return
}
func (c Cater) Ignored(ignore ignorer) Cater {
cat := make(Cater)
cat := make(Cater, 0, len(c))
ok := func(e entry) bool {
return !ignore.Ignore(e.fqname)
}
for name, content := range c {
if ignore.Ignore(name) {
continue
}
cat[name] = content
for _, entry := range c {
cat = append(cat, entry.filter(ok))
}
return cat
@ -47,9 +48,20 @@ func (c Cater) Ignored(ignore ignorer) Cater {
func (c Cater) ToString(delemiter string) string {
var sb strings.Builder
for name, content := range c {
sb.WriteString(fmt.Sprintf(delemiter, name))
sb.WriteString(content)
var entries []entry
entries = c
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()

32
pkg/cat/entry.go Normal file
View 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)
}
}

View File

@ -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
View 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
View 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
View 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()
}

View File

@ -1,9 +1,12 @@
package ignore
import "strings"
import (
"strings"
)
type Filesystem struct{}
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], ".")
}