Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions internal/cli/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cli

import (
"context"
"fmt"
"io"
"os"

"github.com/deref/transcript/internal/core"
"github.com/natefinch/atomic"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(formatCmd)
}

var formatCmd = &cobra.Command{
Use: "format [transcripts...]",
Short: "Formats transcript files",
Long: `Formats transcript files by normalizing comments, blank lines,
trailing whitespace (except in command output), trailing newline,
and special directive syntax.

If no files are provided, reads from stdin and writes to stdout.
If files are provided, formats them in-place.
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if len(args) == 0 {
// Read from stdin, write to stdout
return formatStdin(ctx)
}
// Format files in-place
for _, filename := range args {
if err := formatFile(ctx, filename); err != nil {
return fmt.Errorf("formatting %q: %w", filename, err)
}
}
return nil
},
}

func formatStdin(ctx context.Context) error {
formatter := &core.Formatter{}
transcript, err := formatter.FormatTranscript(ctx, os.Stdin)
if err != nil {
return err
}
_, err = io.Copy(os.Stdout, transcript)
return err
}

func formatFile(ctx context.Context, filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()

formatter := &core.Formatter{}
transcript, err := formatter.FormatTranscript(ctx, f)
if err != nil {
return err
}
return atomic.WriteFile(filename, transcript)
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input file remains open until function return, which can cause file‐lock errors on Windows when renaming over it. Consider closing the reader (f.Close()) before calling atomic.WriteFile.

Copilot uses AI. Check for mistakes.
}
94 changes: 94 additions & 0 deletions internal/core/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package core

import (
"bytes"
"context"
"io"
"strconv"
"strings"
)

type Formatter struct {
buf *bytes.Buffer
}

func (f *Formatter) FormatTranscript(ctx context.Context, r io.Reader) (transcript *bytes.Buffer, err error) {
f.buf = &bytes.Buffer{}

// Use the regular interpreter with the formatter as handler.
interp := &Interpreter{
Handler: f,
}
if err := interp.ExecTranscript(ctx, r); err != nil {
return nil, err
}

// Ensure file ends with exactly one newline
content := f.buf.Bytes()
content = bytes.TrimRight(content, "\n")
if len(content) > 0 {
content = append(content, '\n')
}

return bytes.NewBuffer(content), nil
}

func (f *Formatter) HandleComment(ctx context.Context, text string) error {
// Normalize comments and blank lines
trimmed := strings.TrimSpace(text)
if trimmed == "" {
// Blank line
f.buf.WriteString("\n")
} else if strings.HasPrefix(trimmed, "#") {
// Normalize comment formatting
comment := strings.TrimPrefix(trimmed, "#")
comment = strings.TrimSpace(comment)
if comment == "" {
f.buf.WriteString("#\n")
} else {
f.buf.WriteString("# " + comment + "\n")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer printf over string concatenation

}
}

return nil
}

func (f *Formatter) HandleRun(ctx context.Context, command string) error {
// Write the command with normalized formatting
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments that are sentences should end with periods.

f.buf.WriteString("$ " + strings.TrimSpace(command) + "\n")

return nil
}

func (f *Formatter) HandleOutput(ctx context.Context, fd int, line string) error {
// Output lines preserve their exact content (including whitespace)
f.buf.WriteString(strconv.Itoa(fd) + " " + line + "\n")

return nil
}

func (f *Formatter) HandleFileOutput(ctx context.Context, fd int, filepath string) error {
// File output references with normalized formatting
f.buf.WriteString(strconv.Itoa(fd) + "< " + strings.TrimSpace(filepath) + "\n")

return nil
}

func (f *Formatter) HandleNoNewline(ctx context.Context, fd int) error {
// No-newline directive with normalized formatting
f.buf.WriteString("% no-newline\n")

return nil
}

func (f *Formatter) HandleExitCode(ctx context.Context, exitCode int) error {
// Exit code with normalized formatting
f.buf.WriteString("? " + strconv.Itoa(exitCode) + "\n")

return nil
}

func (f *Formatter) HandleEnd(ctx context.Context) error {
// No special handling needed for end
return nil
}
11 changes: 5 additions & 6 deletions internal/core/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ func (upr *Updater) flushCurrentCommand(ctx context.Context) error {
if upr.currentCommand == "" {
return nil // No command to flush
}

// Set up recorder with file references for this command
upr.rec.SetPreferredFiles(upr.fileRefs)

// Execute the command
if _, err := upr.rec.RunCommand(ctx, upr.currentCommand); err != nil {
return err
}

// Clear the buffer
upr.fileRefs = nil
upr.currentCommand = ""

return nil
}

Expand All @@ -66,7 +66,7 @@ func (upr *Updater) HandleRun(ctx context.Context, command string) error {
if err := upr.flushCurrentCommand(ctx); err != nil {
return err
}

// Buffer this command - don't execute it yet
upr.currentCommand = command
return nil
Expand Down Expand Up @@ -97,4 +97,3 @@ func (upr *Updater) HandleEnd(ctx context.Context) error {
// Flush any remaining command at the end
return upr.flushCurrentCommand(ctx)
}

14 changes: 14 additions & 0 deletions tests/format-basic.cmdt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Test basic formatting

$ echo hello
1 hello

# Multiple blank lines should be normalized


$ echo world
1 world

# Comment with irregular spacing
$ false
? 1
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file doesn't end with a trailing newline. Did you run the formatter you created on these files?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still an issue, but you can run the formatter now

13 changes: 13 additions & 0 deletions tests/format-basic.cmdt.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Test basic formatting

$ echo hello
1 hello

# Multiple blank lines should be normalized

$ echo world
1 world

# Comment with irregular spacing
$ false
? 1
42 changes: 42 additions & 0 deletions tests/format-command.cmdt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Test format command functionality

# Create a file with formatting issues
$ cat > unformatted.cmdt <<'EOF'
#comment without space


$ echo hello
1 hello

# irregular comment spacing
$ false
? 1
EOF

# Test stdin/stdout formatting
$ transcript format < unformatted.cmdt
1 # comment without space
1
1 $ echo hello
1 1 hello
1
1 # irregular comment spacing
1 $ false
1 ? 1

# Test actual formatting (in-place)
$ transcript format unformatted.cmdt

# Verify the file was formatted correctly
$ cat unformatted.cmdt
1 # comment without space
1
1 $ echo hello
1 1 hello
1
1 # irregular comment spacing
1 $ false
1 ? 1

# Clean up
$ rm unformatted.cmdt
21 changes: 21 additions & 0 deletions tests/format-simple.cmdt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Test simple format command

# Create a file with whitespace and formatting issues
$ cat > test.cmdt <<'EOF'
#comment
$ echo test
1 test
#another comment
? 0
EOF

# Format it using stdin/stdout
$ transcript format < test.cmdt
1 # comment
1 $ echo test
1 1 test
1 # another comment
1 ? 0

# Clean up
$ rm test.cmdt