Skip to content

feat: add --extract and --extract-last to output only a fenced code block - #2219

Merged
ksylvan merged 2 commits into
danielmiessler:mainfrom
anandghegde:feat/extract-code-blocks
Sep 29, 2026
Merged

ksylvan merged 2 commits into
danielmiessler:mainfrom
anandghegde:feat/extract-code-blocks

Conversation

@anandghegde

Copy link
Copy Markdown
Contributor

What this Pull Request (PR) does

Adds two output flags:

  • --extract: output only the first Markdown fenced code block in the response
  • --extract-last: output only the last fenced code block

These work like llm -x/--extract and --xl/--extract-last from simonw's llm. They help when a pattern returns code with explanation around it and you only want the code, e.g. to pipe it into a file or a shell.

echo "bash one-liner to find files over 100MB" | fabric --extract > find-big.sh
echo "Dockerfile for a small Go web app" | fabric -s --extract -o Dockerfile

Behaviour:

  • No fenced block found: the full response is output unchanged, same as llm. The help text says this.
  • Where it applies: extraction changes what is printed, copied with -c and written with -o, and what goes into the notification. The session still stores the full response, and --output-session still writes the full session.
  • Both flags given: --extract-last wins, same as llm.
  • Fence parsing: fences can use or `~~~`, and can have a language tag or other info string. A fence can be 4 or more characters long, which lets a block contain a shorter fence. The closing fence must use the same character and be at least as long as the opening one. A fence can be indented by up to 3 spaces. A backtick "fence" followed by more backticks on the same line (inline ````code``` ````) is not treated as a fence. An unterminated last fence runs to the end of the response, so a response cut off by a token limit still gives you its code. This is the one place where it differs from llm, which ignores unterminated blocks.
  • With --stream: extraction needs the whole response, so the chunks are collected without being printed and only the extracted block is printed at the end. This works the same way --suppress-think already does. llm turns streaming off instead. I kept the vendor request streaming so that --show-metadata usage output and stream error messages still appear. It works through a new ChatOptions.BufferStream field that the chatter checks before printing each chunk.

The extraction itself is the pure function domain.ExtractFencedCodeBlock(text, last), which has table tests.

Help text is registered in help.go, and translations are added for all 12 locales, following #2179. The new flags are also added to the zsh, bash and fish completions and to the README flag listing. I'll add the cmd/generate_changelog/incoming/<PR>.txt entry once this PR has a number.

Related issues

None.

Screenshots

Tested against a local fake OpenAI-compatible server (LM Studio vendor). The model reply had a ```python block and a later ~~~bash block:

$ echo hi | fabric --extract
print('first')
$ echo hi | fabric --extract-last
echo last
$ echo hi | fabric -s --extract-last --show-metadata

[Metadata] Input: 3 | Output: 5 | Total: 8
echo last
$ echo hi | fabric -s --extract-last -o out.txt >/dev/null; cat out.txt
echo last
$ echo hi | fabric -s --extract -c >/dev/null; pbpaste
print('first')
$ echo hi | fabric -s --extract          # reply without code block
No code in this reply.

--extract --output-session -o sess.txt still wrote the full assistant reply to the file.

Testing

  • go test ./...: everything passes except TestValidateImageFile in internal/cli. That test also fails on unmodified main on my machine because /tmp/test already exists there, so it is unrelated to this change.
  • New tests: TestExtractFencedCodeBlock (17 cases), TestChatter_Send_StreamingBufferStreamDoesNotPrint (checked that it fails without the chatter change) and TestBuildChatOptionsExtractBuffersStream.
  • go vet ./..., gofmt -l, goimports -l and modernize ./... all report nothing.

Claude Code helped write this change. I reviewed and tested it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QBWFWqjbUQGS1c9LsM2bxX

@anandghegde
anandghegde force-pushed the feat/extract-code-blocks branch from 4b0a61a to ebfff61 Compare September 15, 2026 16:38
…lock

Print, copy (-c) and write (-o) only the first or last Markdown fenced
code block from the model response, like `llm -x/--extract` and
`--xl/--extract-last`. If the response has no fenced code block, the
full response is used unchanged. The session keeps the full response.

With --stream, the streamed content is buffered instead of printed so
that only the extracted block is printed at the end.
@anandghegde
anandghegde force-pushed the feat/extract-code-blocks branch from ebfff61 to 22660fb Compare September 28, 2026 17:25
@ksylvan ksylvan self-assigned this Sep 28, 2026

@ksylvan ksylvan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nicely done @anandghegde

@ksylvan
ksylvan merged commit 38ac533 into danielmiessler:main Sep 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants