Skip to content

Commit 2b77c5e

Browse files
committed
WIP
1 parent 1376501 commit 2b77c5e

File tree

4 files changed

+82
-65
lines changed

4 files changed

+82
-65
lines changed

Filtering.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Change the syntax of `$include` to: `$include(command,arg,...){file}` (and similar for `$paste`). The semantics are that `file` is found and expanded, then the result is fed to `command arg...`'s stdin. If you omit the `{file}`, then you are just running a command and expanding/pasting its output.

README.md

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,25 +110,21 @@ a file.
110110

111111
Nancy expands a template file as follows:
112112

113-
1. Scan the file for commands. Expand any arguments to the command, run
114-
each command, and replace the command by the result.
115-
2. Output the result.
113+
1. Scan the text for commands. Expand any arguments to the command, run each
114+
command, and replace the command by the result, eliding any final
115+
newline. (This elision may look tricky, but it almost always does what
116+
you want, and makes `$include` behave better in various contexts.)
117+
2. Output the resultant text.
116118

117119
A command takes the form `$COMMAND` or `$COMMAND{ARGUMENT, ...}`.
118120

119-
Nancy treats its input as 8-bit ASCII, but command names and other
120-
punctuation only use the 7-bit subset. This means that any text encoding
121-
that is a superset of 7-bit ASCII can be used, such as UTF-8.
122-
123121
### Built-in commands
124122

125123
Nancy recognises these commands:
126124

127125
* *`$include{FILE}`* Look up the given source file in the input tree (see
128126
below); read its contents, then expand them (that is, execute any commands
129-
it contains) and return the result, eliding any final newline. (This
130-
elision may look tricky, but it almost always does what you want, and
131-
makes $include behave better in various contexts.)
127+
it contains) and return the result.
132128
* *`$paste{FILE}`* Like `$include`, but does not expand its result before
133129
returning it.
134130
* *`$path`* Expands to the file currently being expanded, relative to the

README.nancy.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![logo](logo/nancy-small.png) _logo by Silvia Polverini_
44

5-
$paste{/bin/sh,-c,PYTHONPATH=. python -m nancy --version | tail +2 | head -2 | sed -e 's/$/ /'}
5+
$paste(/bin/sh,-c,PYTHONPATH=. python -m nancy --version | tail +2 | head -2 | sed -e 's/$/ /')
66

77
Nancy is a simple templating system that copies a file or directory, filling
88
in templates as it goes. It has just one non-trivial construct:
@@ -35,7 +35,7 @@ $ pip install nancy
3535
## Invocation
3636

3737
```
38-
$paste{/bin/sh,-c,PYTHONPATH=. python -m nancy --help | sed -e 's/usage: nancy/nancy/'}
38+
$paste(/bin/sh,-c,PYTHONPATH=. python -m nancy --help | sed -e 's/usage: nancy/nancy/')
3939
```
4040

4141
## Operation <a name="operation"></a>
@@ -99,7 +99,11 @@ Nancy expands a template file as follows:
9999
each command, and replace the command by the result.
100100
2. Output the result.
101101

102-
A command takes the form `\$COMMAND` or `\$COMMAND{ARGUMENT, ...}`.
102+
A command is written as its name prefixed with a dollar sign: `\$COMMAND`.
103+
Some commands take an argument, given in braces: `\$COMMAND{ARGUMENT}`.
104+
Finally, some commands also take an optional external command and arguments:
105+
`\$COMMAND(EXTERNAL-COMMAND, ARGUMENT, …){ARGUMENT}`; the argument in braces
106+
is optional in this case.
103107

104108
Nancy treats its input as 8-bit ASCII, but command names and other
105109
punctuation only use the 7-bit subset. This means that any text encoding
@@ -152,27 +156,24 @@ worked example.
152156
### Running other programs
153157

154158
In addition to the rules given above, Nancy also allows `\$include` and
155-
`\$paste` to take their input from programs. This can be useful in a variety
156-
of ways: to insert the current date or time, to make a calculation, or to
157-
convert a file to a different format.
159+
`\$paste` to run external programs, whose output becomes the result of the
160+
command. If an input is given, it is supplied to the program’s standard
161+
input. This can be useful in a variety of ways: to insert the current date
162+
or time, to make a calculation, or to convert a file to a different format.
158163

159-
Nancy can run a program in two ways:
164+
Nancy looks for programs in two ways:
160165

161-
1. If a file found by an `\$include` or `\$paste` command has the “execute”
162-
permission, it is run.
166+
1. Using the same rules as for finding an `\$include` or `\$paste` input,
167+
Nancy looks for a file which has the “execute” permission.
163168

164169
2. If no file of the given name can be found using the rules in the previous
165170
section, Nancy looks for an executable file on the user’s `PATH` (the
166-
list of directories specified by the `PATH` environment variable). If one
167-
is found, it is run.
168-
169-
In either case, arguments may be passed to the program: use
170-
`\$include{FILE,ARGUMENT_1,ARGUMENT_2,…}`, or the equivalent for `\$paste`.
171+
list of directories specified by the `PATH` environment variable).
171172

172173
For example, to insert the current date:
173174

174175
```
175-
\$paste{date,+%Y-%m-%d}
176+
\$paste(date,+%Y-%m-%d)
176177
```
177178

178179
See the [date example](Cookbook.md#date-example) in the Cookbook for more
@@ -188,14 +189,13 @@ To prevent a comma from being interpreted as an argument separator, put a
188189
backslash in front of it:
189190

190191
```
191-
\$include{cat,I\, Robot.txt,3 Rules of Robotics.txt}
192+
\$include(cat,I\, Robot.txt,3 Rules of Robotics.txt)
192193
```
193194

194-
This will run the `\$include` command with the following arguments:
195+
This will run the `cat` command with the following arguments:
195196

196-
1. `cat`
197-
2. `I, Robot.txt`
198-
3. `3 Rules of Robotics.txt`
197+
1. `I, Robot.txt`
198+
2. `3 Rules of Robotics.txt`
199199

200200
Note that the filenames supplied to `cat` refer not to the input tree, but
201201
to the file system.

nancy/__init__.py

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,32 @@ def find_object(obj: Path) -> Optional[Union[Path, list[os.DirEntry[str]]]]:
8686
return None
8787
return sorted(list(dirents.values()), key=lambda x: sorting_name(x.name))
8888

89+
def parse_arguments(
90+
text: bytes, arg_start: int, opening: int, closing: int
91+
) -> tuple[list[bytes], int]:
92+
args = []
93+
depth = 1
94+
next_index = arg_start + 1
95+
while next_index < len(text):
96+
if text[next_index] == closing:
97+
depth -= 1
98+
if depth == 0:
99+
args.append(text[arg_start + 1 : next_index])
100+
break
101+
elif text[next_index] == opening:
102+
depth += 1
103+
elif (
104+
depth == 1
105+
and text[next_index] == ord(b",")
106+
and text[next_index - 1] != ord(b"\\")
107+
):
108+
args.append(text[arg_start + 1 : next_index])
109+
arg_start = next_index
110+
next_index += 1
111+
if next_index == len(text):
112+
raise ValueError(f"missing '{closing}'")
113+
return args, next_index + 1
114+
89115
def expand_bytes(
90116
text: bytes,
91117
base_file: Path,
@@ -149,14 +175,16 @@ def read_file(
149175

150176
# Set up macros
151177
macros: dict[bytes, Callable[..., bytes]] = {}
152-
macros[b"path"] = lambda _args: bytes(base_file)
153-
macros[b"realpath"] = lambda _args: bytes(file_path)
178+
macros[b"path"] = lambda _args, _external_args: bytes(base_file)
179+
macros[b"realpath"] = lambda _args, _external_args: bytes(file_path)
154180
macros[b"outputpath"] = (
155-
lambda _args: bytes(output_path) if output_path is not None else b""
181+
lambda _args, _external_args: bytes(output_path)
182+
if output_path is not None
183+
else b""
156184
)
157185

158186
def get_included_file(
159-
command_name: str, args: list[bytes]
187+
command_name: str, args: list[bytes],external_args: list[bytes]
160188
) -> tuple[Optional[Path], bytes]:
161189
debug(f"${command_name}{{{b','.join(args)}}}")
162190
if len(args) < 1:
@@ -166,8 +194,8 @@ def get_included_file(
166194
file = get_file(Path(os.fsdecode(args[0])))
167195
return read_file(file, args[1:])
168196

169-
def include(args: list[bytes]) -> bytes:
170-
file, contents = get_included_file("include", args)
197+
def include(args: list[bytes], external_args: list[bytes]) -> bytes:
198+
file, contents = get_included_file("include", args, external_args)
171199
return strip_final_newline(
172200
inner_expand(
173201
contents, expand_stack + [file] if file is not None else []
@@ -182,19 +210,26 @@ def paste(args: list[bytes]) -> bytes:
182210

183211
macros[b"paste"] = paste
184212

185-
def do_macro(macro: bytes, args: list[bytes]) -> bytes:
186-
debug(f"do_macro {macro} {args}")
213+
def expand_args(args: list[bytes]) -> list[bytes]:
187214
expanded_args: list[bytes] = []
188215
for a in args:
189216
# Unescape escaped commas
190217
debug(f"escaped arg {a}")
191218
unescaped_arg = re.sub(rb"\\,", b",", a)
192219
debug(f"unescaped arg {unescaped_arg}")
193220
expanded_args.append(do_expand(unescaped_arg))
221+
return expanded_args
222+
223+
def do_macro(
224+
macro: bytes, args: list[bytes], external_args: list[bytes]
225+
) -> bytes:
226+
debug(f"do_macro {macro} {args} {external_args}")
227+
expanded_args = expand_args(args)
228+
expanded_external_args = expand_args(external_args)
194229
if macro not in macros:
195230
decoded_macro = macro.decode("iso-8859-1")
196231
raise ValueError(f"no such macro '${decoded_macro}'")
197-
return macros[macro](expanded_args)
232+
return macros[macro](expanded_args, expanded_external_args)
198233

199234
debug("do_match")
200235
startpos = 0
@@ -206,38 +241,23 @@ def do_macro(macro: bytes, args: list[bytes]) -> bytes:
206241
debug(f"match: {res} {res.end()}")
207242
escaped = res[1]
208243
name = res[2]
209-
arg_start = res.end()
210-
startpos = arg_start
244+
startpos = res.end()
211245
args = []
212-
# Parse arguments, respecting nested commands
213-
if arg_start < len(expanded) and expanded[arg_start] == ord(b"{"):
214-
depth = 1
215-
next_index = arg_start + 1
216-
while next_index < len(expanded):
217-
if expanded[next_index] == ord(b"}"):
218-
depth -= 1
219-
if depth == 0:
220-
args.append(expanded[arg_start + 1 : next_index])
221-
break
222-
elif expanded[next_index] == ord(b"{"):
223-
depth += 1
224-
elif (
225-
depth == 1
226-
and expanded[next_index] == ord(b",")
227-
and expanded[next_index - 1] != ord(b"\\")
228-
):
229-
args.append(expanded[arg_start + 1 : next_index])
230-
arg_start = next_index
231-
next_index += 1
232-
if next_index == len(expanded):
233-
raise ValueError("missing close brace")
234-
startpos = next_index + 1
246+
# Parse external program arguments
247+
if startpos < len(expanded) and expanded[startpos] == ord(b"("):
248+
external_args, startpos = parse_arguments(
249+
expanded, startpos, ord("("), ord(")")
250+
)
251+
if startpos < len(expanded) and expanded[startpos] == ord(b"{"):
252+
args, startpos = parse_arguments(
253+
expanded, startpos, ord("{"), ord("}")
254+
)
235255
if escaped != b"":
236256
# Just remove the leading '\'
237257
args_string = b"{" + b",".join(args) + b"}"
238258
output = b"$" + name + (args_string if len(args) > 0 else b"")
239259
else:
240-
output = do_macro(name, args)
260+
output = do_macro(name, args, external_args)
241261
expanded = expanded[: res.start()] + output + expanded[startpos:]
242262
# Update search position to restart matching after output of macro
243263
startpos = res.start() + len(output)

0 commit comments

Comments
 (0)