Sending patch for new feat: placeholders in execute command

Luca Coraggio <[email protected]> Mon, 10 Nov 2025 12:11:34 +0100
Newsgroups gmane.editors.nano.devel
Message-ID <[email protected]>
Dear Devs,

I am sending attached a patch that implements placeholders substitution 
in Nano's execute command.
I give more details below (What, How, and Why). As it ended out to be 
longer than I expected I am postponing them
to the end of the email.

The patch also contains the modified documentation.

I am willing to work on it further, as you require. It is also ok if you 
want to re-implement the feature yourself if you
think my implementation is not good. I am available to provide further 
details or helping with anything.

I really hope you will consider the idea and the possibility to add this 
functionality to Nano.

Thank you for your work on the Nano editor. It is really a joy to work 
with it.

Best regards,
Luca Coraggio (Naples, Italy)

What:
In brief, the patch makes it possible to use special sequences in the 
input string to the execute_command to
expose to the user information already available to nano internal. 
Currently (but I think this is already enough)
it is possible to use "\CWD", "\FTYPE" and "\FNAME" that are replaced by 
"/current/working/directory" (as returned
by getcwd), "openfile->filename" and "openfile->syntax". For example, 
executing the command "echo \FTYPE" on a
Python file will return "python". Two slashes escape: "echo \\CWD" 
results in "CWD" in the editor.

How:
There are two additions: the new struct "cmdplaceholders" 
(definitions.h:667) and theĀ  "alloc_and_parse_command" function (in 
files.c:1017).
These are not defined for NANO_TINY.
The cmdplaceholders defines a new type to hold the placeholder (e.g. 
\CWD) and its replacement string (current working directory).
The alloc_and_parse_command takes in input the string to exectue and: 
(i) computes the length of the new string considering the
deltas in length between the placeholder text and the replacement text; 
(ii) allocates the new string; (iii) copies the original command
substituting the placeholders with their replacements.
This function (and the parsed command) is used ONLY in lines 1412-1414 
of files.c in place of the original command
and the newly allocated command string is freed immediately. The 
function does not alter the history of the saved command.
So that if the original command is: "echo \FTYPE", the actual string 
that is executed is "echo python"; in the history "echo \FTYPE" is saved.

Why:
I think this feature unlocks an highway of possibilities for using nano 
in advanced text editing contexts. At the expense of few new lines of
code, we can expose information that is vital for creating modular 
external scripts (written in shell or whatever) to operate on the text
conditionally on the filetype or filename. This extends the 
functionality already available with the syntax files that allow to 
specify formatter and
linter according to the file type. However, these are not the only 
filetype-specific applications. For example, one could build a shell command
that, according to the filetype, invokes the appropriate documentation 
program in an external terminal (e.g. pydoc for python or R's help in R).
I would use this myself. Another example: we may want to create a shell 
program that asks autocompletion to AI, using the current file as a
context; \CWD and \FNAME enables the user to easily locate and use the 
file content externally (currently we are limited to pipe the entire file
to the shell command in the execute prompt). We can have a shell script 
that uses shell filters to insert relevant snippets for the file type!

I use nano as my main (only) editor for coding. I really appreciate the 
simplicity of nano and I think it is almost feature complete as it is.
This is to say that I appreciate the trade-off between adding a new 
feature and keeping a clean, do-one-thing and efficient program. However,
I really think that this small addition would tremendously improve nano 
capabilities (also for future applications), while maintaining the original
spirit of nano: this is not a plugin-system or something complicated, we 
are simply exposing information that is already available.
0001-placeholders.patch (text/x-patch, 6.7 KB)
From 81b020a3d10c7494145aca23c2e9544b9e0b5384 Mon Sep 17 00:00:00 2001
From: Luca Coraggio <[email protected]>
Date: Sat, 1 Nov 2025 19:05:39 +0100
Subject: [PATCH] Add alloc_and_parse_command (file.c), add cmdplacholders
 structure (definitions.h), updated docs

Rationale -- this commit adds the following new feature: execute command can now use
 placeholders that get replaced with current file information (filename, filetype, or current
 working dir). This enables the user to use external commands (e.g. shell filters) that work
 conditionally on file-specific information. File-specific external commands extend the idea of
 syntax-specific linter and formatter, greatly enhancing nano's flexibility in handling
 different file types, by exposing information information that is already available within nano
 internals, with as little modifications as possible.

Signed-off-by: Luca Coraggio <[email protected]>
---
 doc/nano.texi     |  5 +++
 doc/nanorc.5      |  5 +++
 src/definitions.h | 10 +++++
 src/files.c       | 95 ++++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 114 insertions(+), 1 deletion(-)

diff --git a/doc/nano.texi b/doc/nano.texi
index 1374094d..4846221e 100644
--- a/doc/nano.texi
+++ b/doc/nano.texi
@@ -1580,6 +1580,11 @@ into the current buffer --- or into a new buffer when @kbd{M-F} is toggled.
 When the program's name is preceded by a pipe symbol (@kbd{|}), then the
 current buffer (or the marked region) is piped to the program, and the
 output of the program replaces the buffer (or the marked region).
+Placeholders can be used in the prompt to access current file information.
+Available placeholders are: @code{\FNAME} (replaced with the current file
+name as shown in the title bar); @code{\FTYPE} (replaced with the current
+file type); @code{\CWD} (replaced with the current working directory).
+Placeholders can be escaped with @code{\} (e.g. @code{\\FTYPE}).
 
 @item speller
 Invokes a spell-checking program, either the default @command{hunspell}
diff --git a/doc/nanorc.5 b/doc/nanorc.5
index b7c3d328..dab72c26 100644
--- a/doc/nanorc.5
+++ b/doc/nanorc.5
@@ -723,6 +723,11 @@ into the current buffer \(em or into a new buffer when \fBM\-F\fR is toggled.
 When the program's name is preceded by a pipe symbol (\fB|\fR), then the
 current buffer (or the marked region) is piped to the program, and the
 output of the program replaces the buffer (or the marked region).
+Placeholders can be used in the prompt to access current file information.
+Available placeholders are: \fB\\FNAME\fR (replaced with the current file name as
+shown in the title bar); \fB\\FTYPE\fR (replaced with the current file type);
+\fB\\CWD\fR (replaced with the current working directory). Placeholder can be
+escaped with \fB\\\fR (e.g., \fB\\\\FNAME\fR).
 .TP
 .B speller
 Invokes a spell-checking program, either the default \fBhunspell\fR(1) or GNU
diff --git a/src/definitions.h b/src/definitions.h
index c73b4a82..daa5dc5d 100644
--- a/src/definitions.h
+++ b/src/definitions.h
@@ -662,3 +662,13 @@ typedef struct completionstruct {
 	struct completionstruct *next;
 } completionstruct;
 #endif
+
+#ifndef NANO_TINY
+typedef struct cmdplaceholders {
+	/* Define placeholders strings that gets substituted in execute commands */
+		const char *placeholder;
+			/* The string that is replaced if found in execute command */
+		const char *replacement;
+			/* The replacement value for placeholder */
+} cmdplaceholders;
+#endif
diff --git a/src/files.c b/src/files.c
index da397e6b..fdbc2649 100644
--- a/src/files.c
+++ b/src/files.c
@@ -1012,6 +1012,97 @@ void send_data(const linestruct *line, int fd)
 	fclose(tube);
 }
 
+
+#ifndef NANO_TINY
+char *alloc_and_parse_command(const char *given)
+{
+	if (!given) return copy_of("");
+
+	char *cwd = getcwd(NULL, 0); /* alloc */
+
+	/* Set available placeholders */
+	const cmdplaceholders placeholders[] = {
+		{"\\FNAME", openfile? (openfile->filename ? openfile->filename : "") : "" },
+		{"\\FTYPE", openfile? (openfile->syntax ? openfile->syntax->name : "") : "" },
+		{"\\CWD", cwd ? cwd : ""},
+	};
+
+
+	/* Initialize auxiliary information on placeholder and offsets */
+	size_t num_placeholders = sizeof(placeholders) / sizeof(placeholders[0]);
+	size_t len_placeholders[num_placeholders];
+	size_t len_replacements[num_placeholders];
+	ssize_t len_offsets[num_placeholders];
+
+	for (int i=0; i<num_placeholders; i++){
+		len_placeholders[i] = strlen(placeholders[i].placeholder);
+		len_replacements[i] = strlen(placeholders[i].replacement);
+		len_offsets[i] = len_replacements[i] - len_placeholders[i];
+	}
+
+	ssize_t len_output = (ssize_t)strlen(given);
+
+	/* Pass once through the given command to find placeholders and
+	   compute the length required for the output string */
+	const char *p = given;
+	while (*p) {
+		if ( *p=='\\' ){
+			if (*(p+1)=='\\') {
+				/* Two backslashes escape the placeholder */
+				p += 2;   /* Skip to the next char after the second backslash */
+				continue;
+			}
+			bool matched = FALSE;
+			for (size_t i=0; i<num_placeholders; i++){
+				if (0==strncmp(p, placeholders[i].placeholder, len_placeholders[i])) {
+					matched = TRUE;
+					len_output += len_offsets[i];
+					p += len_placeholders[i];
+					break;
+				}
+			}
+			if (!matched) p++;
+		}
+		else p++;
+	}
+
+	/* Second pass on given: copy string substituting placeholders or escaped placeholders*/
+	if ( (len_output<=0) || (len_output > SSIZE_MAX - 1) ) return copy_of(given);
+
+	char *output = nmalloc(len_output + 1); /* +1 for the null terminator */
+	char *o = output;
+	while (*given){
+		if (*given=='\\'){
+			if ( *(given+1)=='\\' ) {
+				/* Escaped */
+				*o++ = *given++;
+				*o++ = *given++;
+			} else {
+				bool matched = FALSE;
+				for (size_t i=0; i<num_placeholders; i++){
+					if (0==strncmp(given, placeholders[i].placeholder, len_placeholders[i])) {
+					        given += len_placeholders[i];
+					        memcpy(o, placeholders[i].replacement, len_replacements[i]);
+					        o += len_replacements[i];
+					        matched = TRUE;
+						break;
+					}
+				}
+				if (!matched) *o++ = *given++;
+			}
+		}
+		else *o++ = *given++;
+	}
+	*o = '\0';
+
+	if(cwd) free(cwd);
+
+	return output;
+}
+#endif
+
+
+
 /* Execute the given command in a shell. */
 void execute_command(const char *command)
 {
@@ -1318,7 +1409,9 @@ void insert_a_file_or(bool execute)
 				/* If the command is not empty, execute it and read its output
 				 * into the buffer, and add the command to the history list. */
 				if (*answer != '\0') {
-					execute_command(answer);
+					char *parsed_command = alloc_and_parse_command(answer);
+					execute_command(parsed_command);
+					free(parsed_command);
 #ifdef ENABLE_HISTORIES
 					update_history(&execute_history, answer, PRUNE_DUPLICATE);
 #endif
-- 
2.51.0