PlantUML CLI power user — batch, stdin/stdout, CI, incremental builds

puml.online

Most people stop at plantuml file.puml — but the command line can do far more than that. This post distils the “power user” play book: batching, CI, watch, pipes, custom pre-processing.

The minimum command-line knowledge

1
plantuml [options] file-or-files

Common options:

Option Meaning
-tsvg / -tpng / -tpdf / -tlatex Output format
-o dir Output directory
-I path !include search path
-c Standalone SVG (no shared CSS)
-DPLANTUML_CONFIG=... Load a global skin file
-thread N / -no-thread Concurrency (default: JVM cores)
-checkonly Validate, do not render
-pipe Read from stdin / write to stdout
-stdlib Output standard library path
-language List supported languages

1. Single-file pipe: read from stdin

1
2
3
echo "@startuml
Alice -> Bob: hi
@enduml" | plantuml -pipe -tsvg > out.svg

In practice: render a puml string held in memory — pair with curl to fetch remote sources or jq to parse puml blocks in JSON.

2. Render an entire directory

1
plantuml -tsvg -o rendered/ docs/diagrams/

Renders every .puml under docs/diagrams/ to rendered/.

Drawback: recurses into subdirectories, including ones you may not want.

Control recursion

1
plantuml -tsvg -recursive=false -o rendered/ docs/diagrams/*.puml

Without recursion, explicitly list the files you want.

3. Batched concurrency

1
2
3
4
5
# PlantUML itself supports threads
plantuml -tsvg -thread 8 -o rendered/ docs/diagrams/*.puml

# Or coordinate externally
ls docs/diagrams/*.puml | xargs -P 4 -I{} plantuml -tsvg {} -o rendered/

Caveat: xargs -P N spawns N plantuml processes, each a JVM — 4 processes = 4 × JVM startup overhead. Using PlantUML’s built-in multi-threaded mode is more efficient.

Performance tuning

1
2
# 4 cores → 3 threads, leaving 1 for the system
plantuml -tsvg -thread 3 -o rendered/ docs/diagrams/*.puml

4. stdin → multiple output formats

1
cat diagram.puml | plantuml -pipe -tsvg -o /tmp/pipe/

-pipe + output directory: PlantUML writes diagram.svg under /tmp/pipe/ automatically.

But multiple output formats via -pipe doesn’t work — only one format per run.

Render multiple formats in one pass

1
2
3
for fmt in svg png pdf; do
plantuml -t${fmt} -o "rendered-${fmt}" diagram.puml
done

5. Validate-only (CI-friendly)

1
plantuml -checkonly diagram.puml

exit code 0 success; non-zero failure — CI-friendly.

Real CI validation flow

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
set -e
fail=0
for f in $(find docs -name '*.puml'); do
if ! plantuml -checkonly "$f"; then
echo "❌ $f syntax error"
fail=1
fi
done
exit $fail

Faster than -tsvg: parse-only, no render. 100 diagrams in 1 minute in CI.

6. Incremental builds: reuse old diagrams

1
2
plantuml -tsvg -o rendered/ docs/diagrams/*.puml
# PlantUML does NOT detect changes — re-renders all diagrams

DIY incremental:

1
2
3
4
5
6
7
8
#!/bin/bash
# Incremental: only re-render puml whose mtime is newer than the SVG
for puml in docs/diagrams/*.puml; do
svg="rendered/$(basename "$puml" .puml).svg"
if [ ! -f "$svg" ] || [ "$puml" -nt "$svg" ]; then
plantuml -tsvg "$puml" -o rendered/
fi
done

Smarter: remember last hash:

1
2
3
4
5
6
7
8
9
10
11
12
13
store=".cache/puml-hashes.json"
mkdir -p .cache
[ -f "$store" ] || echo '{}' > "$store"

for puml in docs/diagrams/*.puml; do
name=$(basename "$puml" .puml)
cur_hash=$(md5sum "$puml" | cut -d' ' -f1)
prev_hash=$(jq -r ".\"$name\" // \"\"" "$store")
if [ "$cur_hash" != "$prev_hash" ]; then
plantuml -tsvg "$puml" -o rendered/
jq --arg n "$name" --arg h "$cur_hash" '.[$n]=$h' "$store" > "$store.tmp" && mv "$store.tmp" "$store"
fi
done

Dependency changes should also trigger (track !include sub-files):

1
2
3
4
5
6
7
8
# Watch: puml + included files
hash_inputs() {
local puml="$1"
cat "$puml"
grep -h '!include' "$puml" | awk '{print $2}' | while read -r inc; do
[ -f "inc/$inc" ] && cat "inc/$inc"
done | md5sum
}

7. Watch files: auto re-render

fswatch (macOS)

1
fswatch -o docs/diagrams/ | xargs -I{} plantuml -tsvg -o rendered/ docs/diagrams/*.puml

Re-renders on every file change.

inotifywait (Linux)

1
2
3
while inotifywait -e close_write docs/diagrams/; do
plantuml -tsvg -o rendered/ docs/diagrams/
done

Useful dev workflow

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# scripts/dev-watch.sh
# Watch docs/diagrams, auto-render
trap "kill 0" EXIT
while true; do
if inotifywait -qq -e close_write docs/diagrams/; then
echo "→ re-render"
plantuml -tsvg -o public/img/diagrams/ docs/diagrams/*.puml
fi
done

Run this + browser localStorage.setItem('refreshInterval', 5) → “edit-and-see” full flow.

8. Pre-processing: auto-inject skins

CI rendering wants uniform skins without editing each .puml:

1
2
3
4
5
6
7
8
9
10
11
# Inject skinparam right after @startuml
for puml in docs/diagrams/*.puml; do
awk '/^@startuml/ {
print
print "skinparam defaultFontName \"Noto Sans CJK SC\""
next
}
{ print }
' "$puml" > /tmp/processed.puml
plantuml -tsvg /tmp/processed.puml -o rendered/
done

sed quick version

1
2
sed '/^@startuml/a skinparam defaultFontName "Noto Sans CJK SC"' file.puml > /tmp/processed.puml
plantuml -tsvg /tmp/processed.puml

9. Error handling / logging

Default output

1
2
plantuml -tsvg diagram.puml
# Spits a lot of INFO logs

Silent

1
plantuml -tsvg -quiet diagram.puml

Or -SINGLE_LINE_LOG=quiet. CI uses 2>/dev/null.

Verbose

1
plantuml -tsvg -DEBUG diagram.puml 2> debug.log

When debugging .h files / font loading issues.

10. Standard library path

1
2
3
plantuml -stdlib
# Output the stdlib path
# /opt/plantuml/stdlib/

Your puml can !include <C4/Container> to reference standard library diagrams (C4 model, BPMN, Archimate etc.).

Custom stdlib

1
plantuml -stdlib %your-custom-stdlib-path%

11. Use plantuml to read stdin + capture stdout

1
2
echo 'Alice -> Bob' | plantuml -pipe -tsvg
# SVG output to stdout

PlantUML output includes XML headers — just cat it:

1
2
3
svg="$(echo 'Alice -> Bob' | plantuml -pipe -tsvg)"
# Embed in HTML
echo "<div>$svg</div>" > /tmp/page.html

Batch multiple files into one HTML

1
2
3
for puml in files/*.puml; do
echo "<div>$(plantuml -pipe -tsvg < "$puml")</div>"
done > all.html

12. Inject Python / JS pre-processing

If you want plantuml to auto lint / fingerprint / compat-fix before render:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# preprocess(): pre-process puml source
preprocess() {
python3 -c "
import sys
src = sys.stdin.read()
# Global skin
src = src.replace('@startuml', '@startuml\n!theme cyborg', 1)
# Strip comments
src = '\n'.join(l for l in src.split('\n') if not l.strip().startswith(\"' ''\"))
sys.stdout.write(src)
"
}

cat docs/diagrams/system.puml | preprocess | plantuml -pipe -tsvg > out.svg

13. SVG post-processing (CSS / transform / compress)

1
2
3
plantuml -tsvg diagram.puml
# Compress with SVGO
npx svgo -i rendered/diagram.svg -o rendered/diagram.min.svg

Or inject corporate colour:

1
2
3
plantuml -tsvg diagram.puml
sed -i 's|fill="#FFFFFF"|fill="#FAF9F6"|g' rendered/diagram.svg
sed -i 's|font-family="sans-serif"|font-family="Inter"|g' rendered/diagram.svg

14. Complete CI pipeline example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# .github/workflows/diagrams.yml
name: Render PlantUML diagrams
on:
push:
paths: ['docs/diagrams/**']

jobs:
render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install PlantUML
run: |
sudo apt-get update
sudo apt-get install -y fonts-noto-cjk
wget -O plantuml.jar https://github.com/plantuml/plantuml/releases/download/v1.2026.x/plantuml-1.2026.x.jar

- name: Lint
run: |
for f in docs/diagrams/*.puml; do
java -jar plantuml.jar -checkonly "$f" || exit 1
done

- name: Render
run: |
java -jar plantuml.jar -tsvg -thread 4 -o public/img/diagrams/ docs/diagrams/*.puml

- name: Diff
run: |
git diff --exit-code public/img/diagrams/ || {
echo "diagrams changed without puml change, PR must review"
exit 1
}

- name: Commit
run: |
git add public/img/diagrams/
git commit -m "docs: re-render diagrams"
git push

Key: git diff --exit-code makes stale diagrams a build failure.

15. One “covers 80%” script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# scripts/render-puml.sh
# Render docs/diagrams to public/img/diagrams
# Invalidate cache only on puml changes (with !include tracking)
set -e

INPUT_DIR="${1:-docs/diagrams}"
OUTPUT_DIR="${2:-public/img/diagrams}"
mkdir -p "$OUTPUT_DIR"

# PlantUML must be installed
if ! command -v plantuml > /dev/null; then
echo "❌ plantuml not found"
exit 1
fi

# Validate
echo "→ validating puml"
fail=0
while read -r puml; do
if plantuml -checkonly "$puml" > /dev/null 2>&1; then
:
else
echo "❌ $puml"
fail=1
fi
done < <(find "$INPUT_DIR" -name '*.puml' -type f)
[ "$fail" -eq 0 ] || exit 1

# Render
echo "→ rendering"
plantuml -tsvg -thread 4 -o "$OUTPUT_DIR" "$(find "$INPUT_DIR" -name '*.puml')"

# Compress SVG
for svg in "$OUTPUT_DIR"/*.svg; do
[ -f "$svg" ] || continue
npx svgo -i "$svg" -o "$svg" 2>/dev/null || true
done

echo "✓ done"

16. Debug PlantUML CLI itself

1
2
3
4
5
6
7
8
9
# Java startup info
plantuml -version
java -version # Java must be in PATH

# Font debug
plantuml -checkonly -DEBUG diagram.puml 2>&1 | grep -i font

# Stack / debug
plantuml -tsvg -DEBUG -J-Djdk.trace=true diagram.puml 2>&1 | head -50

Troubleshooting specific errors

Error How to fix
Could not find Graphviz Install graphviz, or switch to ELK
Java not found apt install default-jdk
font not found fc-list to see fonts; use -fontpath
diagram not supported Switch to client render / cheat
OutOfMemoryError: Java heap space Add -Xmx2048m JAVA_OPTS

Recap

  • plantuml CLI is not “1 file 1 command” — pipes, watch, concurrency, incremental all have recipes.
  • Incremental is the key to CI efficiency — 8 diagrams vs 200 diagrams is 50× in time.
  • When debugging, plantuml -checkonly -DEBUG is the entry point.
  • Combined with inotifywait + fswatch for “edit-and-see”.

Next

  • Title: PlantUML CLI power user — batch, stdin/stdout, CI, incremental builds
  • Author: puml.online
  • Created at : 2026-07-30 12:31:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-cli-advanced-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.