Skip to content

Commit 2b94019

Browse files
Add format command for .cmdt files
Implements formatting functionality that normalizes: - Comments (ensures proper ''# comment'' spacing) - Commands (removes trailing whitespace) - File output references (standardizes spacing) - Directives (standardizes syntax) - File endings (ensures single trailing newline) Similar to the updater but preserves existing expectations rather than recording new ones. Includes test cases covering basic formatting scenarios. Co-authored-by: Brandon Bloom <[email protected]>
1 parent 219ff42 commit 2b94019

File tree

6 files changed

+258
-0
lines changed

6 files changed

+258
-0
lines changed

internal/cli/format.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/deref/transcript/internal/core"
10+
"github.com/natefinch/atomic"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func init() {
15+
formatCmd.Flags().BoolVarP(&formatFlags.DryRun, "dry-run", "n", false, "dry run")
16+
rootCmd.AddCommand(formatCmd)
17+
}
18+
19+
var formatFlags struct {
20+
DryRun bool
21+
}
22+
23+
var formatCmd = &cobra.Command{
24+
Use: "format <transcripts...>",
25+
Short: "Formats transcript files",
26+
Long: `Formats transcript files by normalizing comments, blank lines,
27+
trailing whitespace (except in command output), trailing newline,
28+
and special directive syntax.
29+
30+
Transcript files are formatted in-place, unless --dry-run is specified. In a dry
31+
run, the formatted output is printed to stdout instead.
32+
`,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
ctx := cmd.Context()
35+
for _, filename := range args {
36+
if err := formatFile(ctx, filename); err != nil {
37+
return fmt.Errorf("formatting %q: %w", filename, err)
38+
}
39+
}
40+
return nil
41+
},
42+
}
43+
44+
func formatFile(ctx context.Context, filename string) error {
45+
f, err := os.Open(filename)
46+
if err != nil {
47+
return err
48+
}
49+
defer f.Close()
50+
51+
formatter := &core.Formatter{}
52+
transcript, err := formatter.FormatTranscript(ctx, f)
53+
if err != nil {
54+
return err
55+
}
56+
if formatFlags.DryRun {
57+
_, err := io.Copy(os.Stdout, transcript)
58+
return err
59+
}
60+
return atomic.WriteFile(filename, transcript)
61+
}

internal/core/formatter.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package core
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
type Formatter struct {
13+
buf *bytes.Buffer
14+
lineno int
15+
}
16+
17+
func (f *Formatter) FormatTranscript(ctx context.Context, r io.Reader) (transcript *bytes.Buffer, err error) {
18+
f.buf = &bytes.Buffer{}
19+
20+
// Use the regular interpreter with the formatter as handler.
21+
interp := &Interpreter{
22+
Handler: f,
23+
}
24+
if err := interp.ExecTranscript(ctx, r); err != nil {
25+
return nil, err
26+
}
27+
28+
// Ensure file ends with exactly one newline
29+
content := f.buf.Bytes()
30+
content = bytes.TrimRight(content, "\n")
31+
if len(content) > 0 {
32+
content = append(content, '\n')
33+
}
34+
35+
return bytes.NewBuffer(content), nil
36+
}
37+
38+
func (f *Formatter) HandleComment(ctx context.Context, text string) error {
39+
f.lineno++
40+
41+
// Normalize comments and blank lines
42+
trimmed := strings.TrimSpace(text)
43+
if trimmed == "" {
44+
// Blank line
45+
f.buf.WriteString("\n")
46+
} else if strings.HasPrefix(trimmed, "#") {
47+
// Normalize comment formatting
48+
comment := strings.TrimPrefix(trimmed, "#")
49+
comment = strings.TrimSpace(comment)
50+
if comment == "" {
51+
f.buf.WriteString("#\n")
52+
} else {
53+
f.buf.WriteString("# " + comment + "\n")
54+
}
55+
}
56+
57+
return nil
58+
}
59+
60+
func (f *Formatter) HandleRun(ctx context.Context, command string) error {
61+
f.lineno++
62+
63+
// Write the command with normalized formatting
64+
f.buf.WriteString("$ " + strings.TrimSpace(command) + "\n")
65+
66+
return nil
67+
}
68+
69+
func (f *Formatter) HandleOutput(ctx context.Context, fd int, line string) error {
70+
f.lineno++
71+
72+
// Output lines preserve their exact content (including whitespace)
73+
f.buf.WriteString(strconv.Itoa(fd) + " " + line + "\n")
74+
75+
return nil
76+
}
77+
78+
func (f *Formatter) HandleFileOutput(ctx context.Context, fd int, filepath string) error {
79+
f.lineno++
80+
81+
// File output references with normalized formatting
82+
f.buf.WriteString(strconv.Itoa(fd) + "< " + strings.TrimSpace(filepath) + "\n")
83+
84+
return nil
85+
}
86+
87+
func (f *Formatter) HandleNoNewline(ctx context.Context, fd int) error {
88+
f.lineno++
89+
90+
// No-newline directive with normalized formatting
91+
f.buf.WriteString("% no-newline\n")
92+
93+
return nil
94+
}
95+
96+
func (f *Formatter) HandleExitCode(ctx context.Context, exitCode int) error {
97+
f.lineno++
98+
99+
// Exit code with normalized formatting
100+
f.buf.WriteString("? " + strconv.Itoa(exitCode) + "\n")
101+
102+
return nil
103+
}
104+
105+
func (f *Formatter) HandleEnd(ctx context.Context) error {
106+
// No special handling needed for end
107+
return nil
108+
}

tests/format-basic.cmdt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Test basic formatting
2+
3+
$ echo hello
4+
1 hello
5+
6+
# Multiple blank lines should be normalized
7+
8+
9+
$ echo world
10+
1 world
11+
12+
# Comment with irregular spacing
13+
$ false
14+
? 1

tests/format-basic.cmdt.expected

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Test basic formatting
2+
3+
$ echo hello
4+
1 hello
5+
6+
7+
$ echo world
8+
1 world
9+
10+
# Comment with irregular spacing
11+
$ false
12+
? 1

tests/format-command.cmdt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Test format command functionality
2+
3+
# Create a file with formatting issues
4+
$ cat > unformatted.cmdt <<'EOF'
5+
#comment without space
6+
7+
8+
$ echo hello
9+
1 hello
10+
11+
# irregular comment spacing
12+
$ false
13+
? 1
14+
EOF
15+
16+
# Test dry-run format
17+
$ transcript format --dry-run unformatted.cmdt
18+
1 # comment without space
19+
1
20+
1 $ echo hello
21+
1 1 hello
22+
1
23+
1 # irregular comment spacing
24+
1 $ false
25+
1 ? 1
26+
27+
# Test actual formatting (in-place)
28+
$ transcript format unformatted.cmdt
29+
30+
# Verify the file was formatted correctly
31+
$ cat unformatted.cmdt
32+
1 # comment without space
33+
1
34+
1 $ echo hello
35+
1 1 hello
36+
1
37+
1 # irregular comment spacing
38+
1 $ false
39+
1 ? 1
40+
41+
# Clean up
42+
$ rm unformatted.cmdt

tests/format-simple.cmdt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Test simple format command
2+
3+
# Create a file with whitespace and formatting issues
4+
$ cat > test.cmdt <<'EOF'
5+
#comment
6+
$ echo test
7+
1 test
8+
#another comment
9+
? 0
10+
EOF
11+
12+
# Format it and show the result
13+
$ transcript format --dry-run test.cmdt
14+
1 # comment
15+
1 $ echo test
16+
1 1 test
17+
1 # another comment
18+
1 ? 0
19+
20+
# Clean up
21+
$ rm test.cmdt

0 commit comments

Comments
 (0)