diff --git a/internal/cli/format.go b/internal/cli/format.go new file mode 100644 index 0000000..4e7e696 --- /dev/null +++ b/internal/cli/format.go @@ -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) +} diff --git a/internal/core/formatter.go b/internal/core/formatter.go new file mode 100644 index 0000000..34c7d1f --- /dev/null +++ b/internal/core/formatter.go @@ -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") + } + } + + return nil +} + +func (f *Formatter) HandleRun(ctx context.Context, command string) error { + // 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 { + // 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 +} 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 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..68b64ee --- /dev/null +++ b/tests/format-basic.cmdt.expected @@ -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 \ No newline at end of file diff --git a/tests/format-command.cmdt b/tests/format-command.cmdt new file mode 100644 index 0000000..16f55f5 --- /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 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 \ No newline at end of file diff --git a/tests/format-simple.cmdt b/tests/format-simple.cmdt new file mode 100644 index 0000000..d759eee --- /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 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 \ No newline at end of file