MD_to_Files.sh β LLM Code Projector#
Turn an LLM answer into a working project in one command. MD_to_Files.sh reads a Markdown response from ChatGPT, Claude, Gemini or DeepSeek and recreates every fenced code block as a real file, with its directories, inside the project folder you choose β instead of copying dozens of files by hand.
./MD_to_Files.sh chatgpt_response.md ./my-new-project
A single Bash script, no runtime dependencies, no package manager, no build step. It parses the response, resolves the target path for each code block, rejects unsafe paths, and writes the files with a summary you can check.
π Table of contents#
π― Why this tool exists#
Ask any assistant for a project and you get a long Markdown answer: a directory tree, then a code block per file. The answer looks complete, but it is only text. Turning it into files means creating each folder, opening each block, pasting, saving under the right name β and repeating that for every file the model produced.
MD_to_Files.sh (also published as LLM Code Projector) automates exactly that step:
| Without the script | With the script |
|---|---|
| 20β50 manual copy-paste operations per project | one command |
| file names and nesting drift from the tree | paths come from the response itself |
encoding surprises (cp1252, latin-1) |
converted to UTF-8 automatically |
../../etc/passwd style paths in model output |
sanitized and rejected before writing |
| no record of what happened | per-file log, --dry-run, --log |
It is a deterministic post-processor for LLM output, not another code generator: the model still writes the code, the script only materialises it.
β¨ What it does#
Extracts fenced code blocks with their language tag (
```js,```python,```jsonβ¦).Resolves the target path from one of three marker formats β see File marker formats.
Creates nested directories exactly as the paths require.
Sanitizes paths: strips terminal escape codes and
< > : " | ? *, removes leading/and../segments, collapses//, truncates names above 255 characters.Normalises encoding:
UTF-8and ASCII pass through,Windows-1252/ISO-8859-1are converted withiconvwhen available.Writes safely: existing files are skipped unless
--forceis given;--backupkeeps a.bak.YYYYmmdd_HHMMSScopy before overwriting.Previews:
--dry-runprints everymkdirand every file it would write, and touches nothing.Reports: per-file status lines, a summary of files/directories/code blocks, optional log file.
Adds project metadata with
--project: a generatedREADME.md(when none exists) and a.llm-projectorfile recording tool version, source file and generation date.
Who it is for: developers prototyping from an assistant's answer, DevOps engineers
generating project skeletons, technical writers turning documentation into runnable
examples, educators preparing course material, and teams that want a repeatable
starting point instead of ad-hoc copy-paste.
System requirements#
| Component | Requirement |
|---|---|
| OS | Linux 3.2+, macOS 10.12+, Windows through WSL / Cygwin / Git Bash, BSD |
| Shell | Bash 4.0 or newer (5.x recommended; macOS ships 3.2 β install a newer Bash) |
| Core utilities | mkdir, find, grep, sed, cat, printf |
| Optional | file (encoding detection), iconv (conversion), readlink (path resolution) |
| Disk | about 100 MB free for generated projects |
Check the environment in one line:
bash --version && for c in mkdir find grep sed file iconv; do
command -v "$c" >/dev/null && echo "β $c" || echo "β $c"; done
π Quick start#
1. Install#
git clone https://github.com/paulmann/1st-Launch-Projects-Code-from-LLM-Responses-in-Markdown-into-Actual-File-Structures.git
cd 1st-Launch-Projects-Code-from-LLM-Responses-in-Markdown-into-Actual-File-Structures
chmod +x MD_to_Files.sh
./MD_to_Files.sh --version
No clone? Fetch the script directly:
curl -L -o ~/bin/md2files \
https://raw.githubusercontent.com/paulmann/1st-Launch-Projects-Code-from-LLM-Responses-in-Markdown-into-Actual-File-Structures/main/MD_to_Files.sh
chmod +x ~/bin/md2files
Optional shell alias:
echo 'alias md2files="$HOME/bin/md2files"' >> ~/.bashrc
source ~/.bashrc
2. Save an LLM answer as Markdown#
# Greeting CLI
## Implementation
```javascript
// [file name]: src/index.js
const name = process.argv[2] ?? 'world';
console.log(`Hello, ${name}!`);
```
```json
// [file name]: package.json
{
"name": "greeting-cli",
"version": "1.0.0"
}
```
3. Run it#
./MD_to_Files.sh greeting.md ./greeting-cli
Real output (v6.0.1):
βΉοΈ INFO Input: greeting.md
βΉοΈ INFO Target: ./greeting-cli
β³ PROGRESS Extracting code blocks from markdown...
β
OK Extracted 2 code block(s)
=== Creating Project Structure from Code Blocks ===
Files to create:
package.json
src/index.js
β³ PROGRESS Creating project structure from code blocks...
βΉοΈ INFO Created directory: ./greeting-cli/src
β
OK Created with content: ./greeting-cli/package.json (50 bytes)
β
OK Created with content: ./greeting-cli/src/index.js (72 bytes)
β
OK Structure from code blocks completed:
βΉοΈ INFO Directories: 1 | Files: 2
β
OK Project structure created successfully
=== SUMMARY ===
β
OK Created: 2 files, 1 directories
βΉοΈ INFO Code blocks: 2 extracted, 2 applied
β¨ Project successfully created/updated!
The exit code is 0 on success and 1 when no code blocks with a resolvable file path were found.
βοΈ How it works#
Read the Markdown file line by line (encoding normalised first).
Open/close fences on every
```line, remembering the language tag.Resolve the target path inside each block β the marker line is consumed and never written to the file.
Sanitize the path and skip anything that tries to leave the target directory.
Create directories, then files, printing one line per artefact and a final summary.
--dry-runstops after step 4.
The directory tree inside the response is not used to build folders β see the next two sections for why that matters.
π·οΈ File marker formats#
The script recognises three ways to say which file a code block belongs to.
1. Comment marker inside the block (recommended)#
// [file name]: src/app.js
console.log('hello');
Also accepted with #, -- and REM prefixes, for Python, SQL/Lua and batch examples:
# [file name]: app/main.py
print('hello')
2. Bare path on the first line of the block#
src/app.js
console.log('hello');
3. Heading with the path, directly above the block#
## src/app.js
```javascript
console.log('hello');
```
What is not recognised#
Bold-only markers are not parsed. Earlier versions of this README recommended **src/app.js** above a block; the parser never had a rule for it, so such blocks are ignored and the run ends with No code blocks found. Use one of the three formats above.
When several blocks target the same path, the last one wins.
π³ Directory trees#
A tree is still useful documentation for the human reading the response, and harmless for the script:
greeting-cli/
βββ src/
β βββ index.js
βββ package.json
Since v6.0.1 the script builds the structure from the file paths in the code blocks and ignores the tree section. This is deliberate: LLM trees frequently disagree with the code that follows (renamed files, extra folders), and creating both produced empty directories nobody asked for. Keep the tree for readability β the folders will be created from the paths.
π» Command-line options#
./MD_to_Files.sh [OPTIONS] INPUT_FILE [TARGET_DIR]
TARGET_DIR defaults to the current directory and is created if missing.
| Option | Long form | Effect |
|---|---|---|
-h |
--help |
Show help |
-V |
--version |
Show version, author, license, Bash version |
-v |
--verbose |
Debug output for every parsed line and resolved path |
-d |
--dry-run |
Print planned mkdir/file writes, change nothing |
-f |
--force |
Overwrite existing files instead of skipping them |
-b |
--backup |
Keep .bak.YYYYmmdd_HHMMSS before overwriting |
-l FILE |
--log FILE |
Append a detailed log to FILE |
-U |
--update |
Update existing files whose content differs |
-P |
--project |
Also generate README.md and .llm-projector metadata |
-C |
--no-code |
Skip code extraction (kept for compatibility, see below) |
-a |
--all |
Reserved, no effect in 6.0.x |
-i |
--interactive |
Reserved, no effect in 6.0.x |
Notes on the last three. --no-code disables the only source of file paths, so a run with -C ends with No code blocks found; it stays in the CLI for backwards compatibility. --all and --interactive were meaningful when the tool still parsed several trees per document; in 6.0.x one document produces one set of files and both flags are accepted but ignored.
Common combinations:
# Preview a risky update
./MD_to_Files.sh -d -v response.md ./project
# Update with backups and a log
./MD_to_Files.sh -U -b -l update.log response.md ./project
# Create with metadata, overwriting generated files
./MD_to_Files.sh -P -f response.md ./project
π¬ Examples#
Node.js CLI (verified output)#
Input and output are shown in Quick start.
React application#
# React Application
## src/index.js
```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
```
## src/App.js
```javascript
export default function App() {
return <h1>Hello from React</h1>;
}
```
## package.json
```json
{
"name": "react-app",
"version": "1.0.0",
"dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }
}
```
./MD_to_Files.sh -P react_app.md ./my-react-app
cd my-react-app && npm install && npm start
Python course, several documents#
./MD_to_Files.sh lesson-1.md ./course-materials
./MD_to_Files.sh lesson-2.md ./course-materials
Each run only adds what its own document contains; existing files are left untouched without --force.
Updating an existing project#
# Files that differ get the new content, identical ones are reported as skipped
./MD_to_Files.sh -U -b response-v2.md ./existing-project
π€ Prompt template for LLMs#
Paste this into the request so the answer is parseable on the first try:
Format the answer so MD_to_Files.sh can turn it into files:
1. Start with "# <Project name>" and a one-paragraph description.
2. Add a "## Project structure" section with a tree in a ``` fence (for humans).
3. For every file, add a heading with its full relative path, then one fenced code block:
## path/to/file.ext
```<language>
<complete file content>
```
Rules:
- every path is relative to the project root, no leading slash;
- use the same path in the heading and in the tree;
- one code block per file, complete content, no placeholders;
- include configuration files (package.json, .env.example, .gitignore).
What to avoid:
Bold-only markers (
**src/app.js**) β not recognised.Several files in one block β only one path can be attached to a block.
Elided content (
// ... rest of the file) β the script writes exactly what the model produced.Paths with
..β stripped by the sanitizer.Non-UTF-8 output β readable, but only
cp1252/latin-1are converted automatically.
π Integration#
Git pre-commit validation#
#!/usr/bin/env bash
# .git/hooks/pre-commit β reject Markdown that cannot be materialised
set -e
for md in $(git diff --cached --name-only --diff-filter=ACM | grep '\.md$' || true); do
./MD_to_Files.sh -d "$md" /tmp/md2files-check >/dev/null || {
echo "β $md has no parsable code blocks"; exit 1; }
done
GitHub Actions#
name: Generate project from Markdown
on:
push:
paths: ['docs/project-structure.md']
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate
run: |
chmod +x ./MD_to_Files.sh
./MD_to_Files.sh -P docs/project-structure.md ./generated
- name: Commit
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add generated/
git commit -m "chore: regenerate from Markdown" || true
git push
VS Code task#
{
"version": "2.0.0",
"tasks": [
{
"label": "Generate files from Markdown",
"type": "shell",
"command": "${workspaceFolder}/MD_to_Files.sh",
"args": ["${file}", "${workspaceFolder}/generated"],
"presentation": { "reveal": "always" }
}
]
}
π§ Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
No code blocks found - cannot create project structure |
no fence carries a resolvable path, or only bold markers were used | add // [file name]: path, a bare path line, or a ## path heading |
File exists, skipping: β¦ |
target already has the file | --force, optionally with --backup |
| Garbled non-Latin text | input was not UTF-8 and not cp1252/latin-1 |
convert first: iconv -f cp1251 -t utf-8 in.md > out.md |
Bash 4.0+ required |
macOS ships Bash 3.2 | brew install bash, then use that binary |
--dry-run created nothing (expected) |
dry-run never writes | run without -d |
| Empty file in the output | the block had a marker but no content | check the fence in the source Markdown |
Diagnostics:
./MD_to_Files.sh -d -v -l debug.log response.md ./preview
grep -E "ERROR|WARN" debug.log
β FAQ#
<details> <summary><b>Does it work with ChatGPT, Claude, Gemini and DeepSeek output?</b></summary>Yes. Any Markdown file with fenced code blocks works, regardless of which assistant produced it. The parser also understands DeepSeek's habit of putting the bare path on the first line of the block.
</details> <details> <summary><b>Can it update a project that already exists?</b></summary>Yes β -U rewrites only the files whose content differs, -b keeps a timestamped backup of each file it touches, and -d shows the plan first.
No. It is a Bash script using coreutils, grep, sed and, when available, file, iconv and readlink.
Since 6.0.1 the tree is documentation only; folders come from the file paths in the code blocks. A folder that contains no file is not created.
</details> <details> <summary><b>Is it safe to run on untrusted model output?</b></summary>Paths are sanitized: control characters, < > : " | ? *, leading / and ../ segments are removed, and files are written only inside TARGET_DIR. Use --dry-run first if you want to see the plan.
Through WSL, Cygwin or Git Bash. Native cmd.exe/PowerShell are not supported.
PolyForm Noncommercial License 1.0.0 β free for noncommercial use, commercial use requires a separate license. See LICENSE.
</details>π Documentation#
| Document | What is inside |
|---|---|
| docs/INDEX.md | Map of the documentation set and reading paths |
| docs/QUICK_REFERENCE.md | One-page cheat sheet: marker syntax, checklist, common mistakes |
| docs/PROMPT_TEMPLATES.md | Copy-paste prompts for Python, JavaScript, full-stack and Docker projects |
| docs/PROMPT_GUIDE.md | Full reference on prompt formatting and validation |
| docs/EXAMPLES.md | Worked examples from request to generated project |
| docs/RUSSIAN_QUICK_START.md | ΠΡΠ°ΡΠΊΠ°Ρ ΠΈΠ½ΡΡΡΡΠΊΡΠΈΡ Π½Π° ΡΡΡΡΠΊΠΎΠΌ |
| docs/PROMPTS.txt | The same prompts as plain text |
| docs/MANIFEST.md | Description of the documentation package |
| CHANGELOG.md | Version history |
| CONTRIBUTING.md | How to propose a change |
| CODE_OF_CONDUCT.md | Community rules |
π€ Contributing#
Bug reports, prompt-format edge cases and pull requests are welcome β see CONTRIBUTING.md. Useful reports include the input Markdown (or a minimal fragment), the command line, and the output of --dry-run -v.
π License#
PolyForm Noncommercial License 1.0.0. Noncommercial use, modification and distribution are permitted under the terms in LICENSE. Commercial use requires a separate license from the author.
Copyright (c) 2025β2026 Mikhail Deynekin.
π Contact#
Author: Mikhail Deynekin
π Website: deynekin.com
π§ Email: mid1977@gmail.com
πΌ LinkedIn: linkedin.com/in/mikhail-deynekin
Project:
π Repository: 1st-Launch-Projects-Code-from-LLM-Responses-in-Markdown-into-Actual-File-Structures
π Issues: report a problem
Made for people who are tired of copy-pasting code out of chat windows.
</div>