8 Commits

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
158e963c7b version2 (#1)
Co-authored-by: u80864958 <niklas.breitenstein@bit.admin.ch>
Reviewed-on: #1
2025-04-02 17:29:26 +02:00
11 changed files with 269 additions and 91 deletions

1
.gitignore vendored
View File

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

2
.vscode/launch.json vendored
View File

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

View File

@ -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.

View File

@ -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 {

View File

@ -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
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 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], ".")
} }