Skip to content

Commit fa517e0

Browse files
committed
add support for multiple data files as args
Closes #137
1 parent 7884406 commit fa517e0

File tree

7 files changed

+277
-111
lines changed

7 files changed

+277
-111
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
dist/
44
.venv/
55
.direnv/
6+
__pycache__/

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ The CLI for [Jinja2](https://jinja.palletsprojects.com/).
44

55
```
66
$ jinja2 template.j2 data.json
7-
$ cat data.json | jinja2 template.j2
87
$ curl -s http://api.example.com | jinja2 template.j2
98
```
109

docs/examples.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,48 @@ $ jinja2 template.j2 data.yaml --format yaml
3939
$ cat data.json | jinja2 template.j2 - --format json
4040
```
4141

42+
## Multiple data files
43+
Merge multiple data files together. Later files override values from earlier files using deep merge:
44+
45+
```sh
46+
$ jinja2 template.j2 base.json overrides.yaml production.json
47+
```
48+
49+
Example with nested structure:
50+
51+
`base.json`:
52+
```json
53+
{
54+
"app": "myapp",
55+
"server": {
56+
"host": "localhost",
57+
"port": 3000
58+
},
59+
"debug": false
60+
}
61+
```
62+
63+
`production.yaml`:
64+
```yaml
65+
server:
66+
port: 8080
67+
debug: false
68+
```
69+
70+
Result after merge:
71+
```json
72+
{
73+
"app": "myapp",
74+
"server": {
75+
"host": "localhost",
76+
"port": 8080
77+
},
78+
"debug": false
79+
}
80+
```
81+
82+
Note that `server.host` is preserved from `base.json` while `server.port` is overridden by `production.yaml`.
83+
4284
## Inline variables
4385
```sh
4486
$ jinja2 template.j2 data.json --format json -D foo=bar -D answer=42
@@ -138,6 +180,15 @@ $ echo 'Home: {{ environ("HOME") }}' | jinja2 -S
138180
Home: /home/user
139181
```
140182

183+
Stream mode also supports data files:
184+
```
185+
$ echo 'Hello {{ name }}!' | jinja2 -S data.json
186+
Hello World!
187+
188+
$ echo '{{ greeting }} {{ name }}!' | jinja2 -S base.json overrides.yaml
189+
Hello World!
190+
```
191+
141192
## In the wild
142193

143194
### Dangerzone

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
packages = with pkgs; [
2929
just
3030
hyperfine
31-
bats
31+
(bats.withLibraries (p: [
32+
p.bats-support
33+
p.bats-assert
34+
p.bats-file
35+
]))
3236
uv
3337
shellcheck
3438
uvShellHook

jinja2cli/cli.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -464,15 +464,41 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
464464
template_path: str | None = None
465465

466466
if opts.stream:
467-
# Stream mode: read template from stdin, no data file
467+
# Stream mode: read template from stdin, all args are data files
468468
template_string = sys.stdin.read()
469-
data: dict | str = {}
469+
data_files = args
470470
else:
471-
template_path_arg, data = args
471+
# Normal mode: first arg is template, rest are data files
472+
template_path_arg = args[0]
473+
data_files = args[1:]
474+
template_path = os.path.abspath(template_path_arg)
475+
476+
data: dict = {}
477+
478+
# Determine if we're reading from stdin or files
479+
if not data_files:
480+
# No data files specified
481+
if opts.stream:
482+
# In stream mode, stdin is used for template, so no data
483+
data_files = []
484+
else:
485+
# Normal mode, read data from stdin
486+
data_files = ["-"]
487+
488+
# Check for invalid mixing of stdin and files
489+
has_stdin = any(f in ("-", "") for f in data_files)
490+
if has_stdin and len(data_files) > 1:
491+
sys.stderr.write("ERROR: Cannot mix stdin (-) with file arguments\n")
492+
return 1
493+
494+
# Load and merge multiple data files
495+
for data_file in data_files:
472496
format = opts.format
473-
if data in ("-", ""):
474-
if data == "-" or (data == "" and not sys.stdin.isatty()):
475-
data = sys.stdin.read()
497+
data_content = ""
498+
499+
if data_file in ("-", ""):
500+
if data_file == "-" or (data_file == "" and not sys.stdin.isatty()):
501+
data_content = sys.stdin.read()
476502
if format == "auto":
477503
# default to yaml first if available since yaml
478504
# is a superset of json
@@ -481,7 +507,7 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
481507
else:
482508
format = "json"
483509
else:
484-
path = os.path.join(os.getcwd(), os.path.expanduser(data))
510+
path = os.path.join(os.getcwd(), os.path.expanduser(data_file))
485511
if format == "auto":
486512
ext = os.path.splitext(path)[1][1:]
487513
if has_format(ext):
@@ -490,11 +516,9 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
490516
raise InvalidDataFormat(ext)
491517

492518
with open(path) as fp:
493-
data = fp.read()
494-
495-
template_path = os.path.abspath(template_path_arg)
519+
data_content = fp.read()
496520

497-
if data:
521+
if data_content:
498522
try:
499523
fn, except_exc, raise_exc = get_format(format)
500524
except InvalidDataFormat:
@@ -510,11 +534,10 @@ def cli(opts: argparse.Namespace, args: Sequence[str]) -> int:
510534
raise InvalidDataFormat("json5: install json5 to fix")
511535
raise
512536
try:
513-
data = fn(data) or {}
537+
parsed = fn(data_content) or {}
538+
deep_merge(data, parsed)
514539
except except_exc:
515-
raise raise_exc(f"{data[:60]} ...")
516-
else:
517-
data = {}
540+
raise raise_exc(f"{data_content[:60]} ...")
518541

519542
extensions = []
520543
for ext in opts.extensions:
@@ -763,16 +786,13 @@ def main() -> None:
763786
dest="stream",
764787
)
765788
parser.add_argument("template", nargs="?", help=argparse.SUPPRESS)
766-
parser.add_argument("data", nargs="?", help=argparse.SUPPRESS)
789+
parser.add_argument("data", nargs="*", help=argparse.SUPPRESS)
767790
opts = parser.parse_args()
768-
args = [value for value in (opts.template, opts.data) if value is not None]
791+
args = [opts.template] + list(opts.data) if opts.template else []
769792

770793
opts.extensions = set(opts.extensions)
771794

772-
if opts.stream:
773-
# Stream mode: no positional args needed
774-
args = []
775-
else:
795+
if not opts.stream:
776796
if len(args) == 0:
777797
parser.print_help()
778798
sys.exit(1)

0 commit comments

Comments
 (0)