From 80cf9ee7d7a37e26e1a48b4e759c203ad1d83665 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 05:43:45 +0000 Subject: [PATCH 1/2] 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 --- internal/cli/format.go | 61 +++++++++++++++++ internal/core/formatter.go | 108 +++++++++++++++++++++++++++++++ tests/format-basic.cmdt | 14 ++++ tests/format-basic.cmdt.expected | 12 ++++ tests/format-command.cmdt | 42 ++++++++++++ tests/format-simple.cmdt | 21 ++++++ 6 files changed, 258 insertions(+) create mode 100644 internal/cli/format.go create mode 100644 internal/core/formatter.go create mode 100644 tests/format-basic.cmdt create mode 100644 tests/format-basic.cmdt.expected create mode 100644 tests/format-command.cmdt create mode 100644 tests/format-simple.cmdt diff --git a/internal/cli/format.go b/internal/cli/format.go new file mode 100644 index 0000000..184cf07 --- /dev/null +++ b/internal/cli/format.go @@ -0,0 +1,61 @@ +package cli + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/deref/transcript/internal/core" + "github.com/natefinch/atomic" + "github.com/spf13/cobra" +) + +func init() { + formatCmd.Flags().BoolVarP(&formatFlags.DryRun, "dry-run", "n", false, "dry run") + rootCmd.AddCommand(formatCmd) +} + +var formatFlags struct { + DryRun bool +} + +var formatCmd = &cobra.Command{ + Use: "format ", + 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. + +Transcript files are formatted in-place, unless --dry-run is specified. In a dry +run, the formatted output is printed to stdout instead. +`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + for _, filename := range args { + if err := formatFile(ctx, filename); err != nil { + return fmt.Errorf("formatting %q: %w", filename, err) + } + } + return nil + }, +} + +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 + } + if formatFlags.DryRun { + _, err := io.Copy(os.Stdout, transcript) + return err + } + return atomic.WriteFile(filename, transcript) +} \ No newline at end of file diff --git a/internal/core/formatter.go b/internal/core/formatter.go new file mode 100644 index 0000000..d101627 --- /dev/null +++ b/internal/core/formatter.go @@ -0,0 +1,108 @@ +package core + +import ( + "bytes" + "context" + "fmt" + "io" + "strconv" + "strings" +) + +type Formatter struct { + buf *bytes.Buffer + lineno int +} + +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 { + f.lineno++ + + // 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") + } + } + + return nil +} + +func (f *Formatter) HandleRun(ctx context.Context, command string) error { + f.lineno++ + + // Write the command with normalized formatting + f.buf.WriteString("$ " + strings.TrimSpace(command) + "\n") + + return nil +} + +func (f *Formatter) HandleOutput(ctx context.Context, fd int, line string) error { + f.lineno++ + + // 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 { + f.lineno++ + + // 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 { + f.lineno++ + + // No-newline directive with normalized formatting + f.buf.WriteString("% no-newline\n") + + return nil +} + +func (f *Formatter) HandleExitCode(ctx context.Context, exitCode int) error { + f.lineno++ + + // 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 +} \ No newline at end of file diff --git a/tests/format-basic.cmdt b/tests/format-basic.cmdt new file mode 100644 index 0000000..4ecaabc --- /dev/null +++ b/tests/format-basic.cmdt @@ -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 \ No newline at end of file diff --git a/tests/format-basic.cmdt.expected b/tests/format-basic.cmdt.expected new file mode 100644 index 0000000..91bcc19 --- /dev/null +++ b/tests/format-basic.cmdt.expected @@ -0,0 +1,12 @@ +# Test basic formatting + +$ echo hello +1 hello + + +$ echo world +1 world + +# Comment with irregular spacing +$ false +? 1 \ No newline at end of file diff --git a/tests/format-command.cmdt b/tests/format-command.cmdt new file mode 100644 index 0000000..2fec560 --- /dev/null +++ b/tests/format-command.cmdt @@ -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 dry-run format +$ transcript format --dry-run 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 \ No newline at end of file diff --git a/tests/format-simple.cmdt b/tests/format-simple.cmdt new file mode 100644 index 0000000..100383f --- /dev/null +++ b/tests/format-simple.cmdt @@ -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 and show the result +$ transcript format --dry-run test.cmdt +1 # comment +1 $ echo test +1 1 test +1 # another comment +1 ? 0 + +# Clean up +$ rm test.cmdt \ No newline at end of file From a01992de01152571d36c2a7ab14e303587cf4202 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:47:13 +0000 Subject: [PATCH 2/2] Enhance format command with stdin/stdout support and remove dry-run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dry-run functionality from format command - Implement stdin/stdout behavior when no files provided - Support in-place formatting when files are specified - Fix formatter to properly handle trailing newlines - Update test files to reflect new stdin/stdout behavior - Remove unused lineno field from formatter Co-Authored-By: Brandon Bloom 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/cli/format.go | 32 ++++++++++++++++------------ internal/core/formatter.go | 36 ++++++++++---------------------- internal/core/updater.go | 11 +++++----- tests/format-basic.cmdt.expected | 1 + tests/format-command.cmdt | 4 ++-- tests/format-simple.cmdt | 4 ++-- 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/internal/cli/format.go b/internal/cli/format.go index 184cf07..4e7e696 100644 --- a/internal/cli/format.go +++ b/internal/cli/format.go @@ -12,26 +12,26 @@ import ( ) func init() { - formatCmd.Flags().BoolVarP(&formatFlags.DryRun, "dry-run", "n", false, "dry run") rootCmd.AddCommand(formatCmd) } -var formatFlags struct { - DryRun bool -} - var formatCmd = &cobra.Command{ - Use: "format ", + 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. -Transcript files are formatted in-place, unless --dry-run is specified. In a dry -run, the formatted output is printed to stdout instead. +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) @@ -41,6 +41,16 @@ run, the formatted output is printed to stdout instead. }, } +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 { @@ -53,9 +63,5 @@ func formatFile(ctx context.Context, filename string) error { if err != nil { return err } - if formatFlags.DryRun { - _, err := io.Copy(os.Stdout, transcript) - return err - } return atomic.WriteFile(filename, transcript) -} \ No newline at end of file +} diff --git a/internal/core/formatter.go b/internal/core/formatter.go index d101627..34c7d1f 100644 --- a/internal/core/formatter.go +++ b/internal/core/formatter.go @@ -3,20 +3,18 @@ package core import ( "bytes" "context" - "fmt" "io" "strconv" "strings" ) type Formatter struct { - buf *bytes.Buffer - lineno int + 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, @@ -24,20 +22,18 @@ func (f *Formatter) FormatTranscript(ctx context.Context, r io.Reader) (transcri 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 { - f.lineno++ - // Normalize comments and blank lines trimmed := strings.TrimSpace(text) if trimmed == "" { @@ -53,56 +49,46 @@ func (f *Formatter) HandleComment(ctx context.Context, text string) error { f.buf.WriteString("# " + comment + "\n") } } - + return nil } func (f *Formatter) HandleRun(ctx context.Context, command string) error { - f.lineno++ - // Write the command with normalized formatting f.buf.WriteString("$ " + strings.TrimSpace(command) + "\n") - + return nil } func (f *Formatter) HandleOutput(ctx context.Context, fd int, line string) error { - f.lineno++ - // 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 { - f.lineno++ - // 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 { - f.lineno++ - // No-newline directive with normalized formatting f.buf.WriteString("% no-newline\n") - + return nil } func (f *Formatter) HandleExitCode(ctx context.Context, exitCode int) error { - f.lineno++ - // 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 -} \ No newline at end of file +} diff --git a/internal/core/updater.go b/internal/core/updater.go index f3feac3..2397105 100644 --- a/internal/core/updater.go +++ b/internal/core/updater.go @@ -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 } @@ -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 @@ -97,4 +97,3 @@ func (upr *Updater) HandleEnd(ctx context.Context) error { // Flush any remaining command at the end return upr.flushCurrentCommand(ctx) } - diff --git a/tests/format-basic.cmdt.expected b/tests/format-basic.cmdt.expected index 91bcc19..68b64ee 100644 --- a/tests/format-basic.cmdt.expected +++ b/tests/format-basic.cmdt.expected @@ -3,6 +3,7 @@ $ echo hello 1 hello +# Multiple blank lines should be normalized $ echo world 1 world diff --git a/tests/format-command.cmdt b/tests/format-command.cmdt index 2fec560..16f55f5 100644 --- a/tests/format-command.cmdt +++ b/tests/format-command.cmdt @@ -13,8 +13,8 @@ $ false ? 1 EOF -# Test dry-run format -$ transcript format --dry-run unformatted.cmdt +# Test stdin/stdout formatting +$ transcript format < unformatted.cmdt 1 # comment without space 1 1 $ echo hello diff --git a/tests/format-simple.cmdt b/tests/format-simple.cmdt index 100383f..d759eee 100644 --- a/tests/format-simple.cmdt +++ b/tests/format-simple.cmdt @@ -9,8 +9,8 @@ $ echo test ? 0 EOF -# Format it and show the result -$ transcript format --dry-run test.cmdt +# Format it using stdin/stdout +$ transcript format < test.cmdt 1 # comment 1 $ echo test 1 1 test