Hi!

Once again, a set of updated patches.

> well, you cut away the part where i kind of implied that it really 
> should be IncludeCmd. (it kind of is awkward, but i think consistency 
> trumps that.)

That might've been a bit too implicit for me. ;) Changed it to IncludeCmd.

> 
> >> on that matter, have you tested all error scenarios?
> >
> >The ones I added? Or all that were modified?
> >
> modified only to the degree that the change cannot be confidently 
> claimed to be irrelevant. so if you changed the output format or the 
> control flow, that would need testing.
> 
> >On the ones that I added,
> >yes, except for failure of popen and the `ret < 0` case in `eval_pclose`.
> >I have to admit I do not really know how to trigger those.
> >
> yeah, that's kind of impossible without some error injection via ptrace 
> or some such. but you can fake the failure by just hacking the code. i 
> wouldn't bother with the 3rd pclose branch, but popen should be tested.

Spent some more time testing. Everything looks good to me.

> >I don't suppose you have some formatter config file?
> >
> bah! Real Hackers (TM) write their own formatters as shell-perl hybrids 
> with recursive regular expressions. ON PUNCH CARDS.
> (i can post it if you're really curious, but you need to sign a 
> liability waiver. ;-)

I'll trade you my magnetic needles so you can do it directly on disk
next time. ;)

Maybe just add it to the repository?

> >(Hopefully) All your other proposed changes were incorporated in these
> >revised patches.
> >
> mostly. but as it goes, the longer you look, the more you find. :-D

Yeah, on that note, thanks again for all the patience!

> >+      fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
> >+      va_start( va, fmt );
> >+      vfprintf( stderr, fmt, va );
> >
> totally on a tangent ...
> i have a bit of an aversion towards tearing apart format strings. it 
> isn't a big deal here, because we're just printing to a stream with no 
> concurrency, but if the target was syslog or a dynamically allocated 
> string, things would look differently. this could be avoided by printing 
> the "payload" to a temporary, but this would obviously have a runtime 
> cost, esp. when using nfasprintf() to be super-safe. but as we already 
> have xvprintf_core(), this overhead could be avoided by supporting 
> recursive format strings: xprintf("%s:%d: %@\n", file, line, fmt, va).
> of course this would be total overkill here ... though once you have it, 
> all kinds of use cases pop up. just thinking aloud ...

Not fixed yet due to 1: the next point, and 2: do you want this? It would
introduce some duplication because conf_error and conf_sys_error print
to stderr and a buffer respectively. But if you see a way to avoid this,
I'd like to hear it.

> 
> >+void
> >+conf_sys_error( conffile_t *cfile, const char *fmt, ... )
> >+{
> >+      va_list va;
> >+
> >+      fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
> >+      va_start( va, fmt );
> >+      vsys_error( fmt, va );
> >
> that call is theoretically unsafe; see the implementation.

Sorry, my lack of experience with C is showing here. I looked at
vsys_error, but are not sure what you mean. Is it the `abort`? Do I fix
it with inlining `vsys_error` and a dynamically allocated buffer?

> signal %d",... WTERMSIG().
> pedantically, that quoting is insufficient because it doesn't do 
> escaping, but there's no need to overdo it here.

That's exactly the reason my inner pedant thought it was best to use
the previous version. But, changed it.

> this isn't quite right; it would miss excessive arguments.
> you can probably use `return getcline()` (and rely on the compiler 
> eliding the tail recursion).

Yes. Because the error message needs to show the correct line
(`include_fp` must still be null when printing the error message)
I reorganized the code a bit.

> come to think of it, you were probably right to make a separate section 
> for it. but it may be better to put it at the end, after the global one 
> - it's kind of more like the global options. otoh, it's kind of a 
> syntactic/structural part of the config, so having it at the top also 
> makes sense. hmm.

Well, I now put your suggestion verbatim at the bottom. Choices...

Do you think that the `pclose` branch `ret < 0` error shouldn't be a
`conf_sys_error` but the normal one (`sys_error`)?

Cheers!
>From 5c47b627095a5b69c7096f667dfd441f86e163d6 Mon Sep 17 00:00:00 2001
From: Michiel van den Heuvel <michielvdnheu...@gmail.com>
Date: Thu, 10 Aug 2023 05:20:55 +0200
Subject: [PATCH 1/2] Refactor printing configuration errors

In preparation for adding the output of commands as configuration lines,
which will complicate printing.
---
 src/config.c      | 116 +++++++++++++++++++++-------------------------
 src/config.h      |   3 ++
 src/driver.c      |   6 +--
 src/drv_imap.c    |  78 ++++++++++++-------------------
 src/drv_maildir.c |  18 +++----
 5 files changed, 93 insertions(+), 128 deletions(-)

diff --git a/src/config.c b/src/config.c
index 456bd47..2995458 100644
--- a/src/config.c
+++ b/src/config.c
@@ -61,6 +61,31 @@ expand_strdup( const char *s, const conffile_t *cfile )
 	}
 }
 
+void
+conf_error( conffile_t *cfile, const char *fmt, ... )
+{
+	va_list va;
+
+	flushn();
+	fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
+	va_start( va, fmt );
+	vfprintf( stderr, fmt, va );
+	va_end( va );
+	cfile->err = 1;
+}
+
+void
+conf_sys_error( conffile_t *cfile, const char *fmt, ... )
+{
+	va_list va;
+
+	fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
+	va_start( va, fmt );
+	vsys_error( fmt, va );
+	va_end( va );
+	cfile->err = 1;
+}
+
 char *
 get_arg( conffile_t *cfile, int required, int *comment )
 {
@@ -75,10 +100,8 @@ get_arg( conffile_t *cfile, int required, int *comment )
 	if (!c || c == '#') {
 		if (comment)
 			*comment = (c == '#');
-		if (required) {
-			error( "%s:%d: parameter missing\n", cfile->file, cfile->line );
-			cfile->err = 1;
-		}
+		if (required)
+			conf_error( cfile, "parameter missing\n" );
 		ret = NULL;
 	} else {
 		for (escaped = 0, quoted = 0, ret = t = p; c; c = *p) {
@@ -98,13 +121,11 @@ get_arg( conffile_t *cfile, int required, int *comment )
 		}
 		*t = 0;
 		if (escaped) {
-			error( "%s:%d: unterminated escape sequence\n", cfile->file, cfile->line );
-			cfile->err = 1;
+			conf_error( cfile, "unterminated escape sequence\n" );
 			ret = NULL;
 		}
 		if (quoted) {
-			error( "%s:%d: missing closing quote\n", cfile->file, cfile->line );
-			cfile->err = 1;
+			conf_error( cfile, "missing closing quote\n" );
 			ret = NULL;
 		}
 	}
@@ -124,9 +145,7 @@ parse_bool( conffile_t *cfile )
 	    strcasecmp( cfile->val, "false" ) &&
 	    strcasecmp( cfile->val, "off" ) &&
 	    strcmp( cfile->val, "0" )) {
-		error( "%s:%d: invalid boolean value '%s'\n",
-		       cfile->file, cfile->line, cfile->val );
-		cfile->err = 1;
+		conf_error( cfile, "invalid boolean value '%s'\n", cfile->val );
 	}
 	return 0;
 }
@@ -139,9 +158,7 @@ parse_int( conffile_t *cfile )
 
 	ret = strtol( cfile->val, &p, 10 );
 	if (*p) {
-		error( "%s:%d: invalid integer value '%s'\n",
-		       cfile->file, cfile->line, cfile->val );
-		cfile->err = 1;
+		conf_error( cfile, "invalid integer value '%s'\n", cfile->val );
 		return 0;
 	}
 	return ret;
@@ -161,9 +178,7 @@ parse_size( conffile_t *cfile )
 	if (*p == 'b' || *p == 'B')
 		p++;
 	if (*p) {
-		fprintf (stderr, "%s:%d: invalid size '%s'\n",
-		         cfile->file, cfile->line, cfile->val);
-		cfile->err = 1;
+		conf_error( cfile, "invalid size '%s'\n", cfile->val);
 		return 0;
 	}
 	return ret;
@@ -249,9 +264,7 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 			} else if (!strcasecmp( "None", arg ) || !strcasecmp( "Noop", arg )) {
 				conf->ops[F] |= XOP_TYPE_NOOP;
 			} else {
-				error( "%s:%d: invalid Sync arg '%s'\n",
-				       cfile->file, cfile->line, arg );
-				cfile->err = 1;
+				conf_error( cfile, "invalid Sync arg '%s'\n", arg );
 			}
 		} while ((arg = get_arg( cfile, ARG_OPTIONAL, NULL )));
 		conf->ops[F] |= XOP_HAVE_TYPE;
@@ -263,15 +276,12 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 		conf->max_messages = parse_int( cfile );
 	} else if (!strcasecmp( "ExpireSide", cfile->cmd )) {
 		arg = cfile->val;
-		if (!strcasecmp( "Far", arg )) {
+		if (!strcasecmp( "Far", arg ))
 			conf->expire_side = F;
-		} else if (!strcasecmp( "Near", arg )) {
+		else if (!strcasecmp( "Near", arg ))
 			conf->expire_side = N;
-		} else {
-			error( "%s:%d: invalid ExpireSide argument '%s'\n",
-			       cfile->file, cfile->line, arg );
-			cfile->err = 1;
-		}
+		else
+			conf_error( cfile, "invalid ExpireSide argument '%s'\n", arg );
 	} else if (!strcasecmp( "ExpireUnread", cfile->cmd )) {
 		conf->expire_unread = parse_bool( cfile );
 	} else {
@@ -295,9 +305,7 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 					} else if (!strcasecmp( "None", arg )) {
 						conf->ops[F] |= op * (XOP_EXPUNGE_NOOP / OP_EXPUNGE);
 					} else {
-						error( "%s:%d: invalid %s arg '%s'\n",
-						       cfile->file, cfile->line, boxOps[i].name, arg );
-						cfile->err = 1;
+						conf_error( cfile, "invalid %s arg '%s'\n", boxOps[i].name, arg );
 					}
 				} while ((arg = get_arg( cfile, ARG_OPTIONAL, NULL )));
 				conf->ops[F] |= op * (XOP_HAVE_EXPUNGE / OP_EXPUNGE);
@@ -315,10 +323,8 @@ getcline( conffile_t *cfile )
 	char *arg;
 	int comment;
 
-	if (cfile->rest && (arg = get_arg( cfile, ARG_OPTIONAL, NULL ))) {
-		error( "%s:%d: excess token '%s'\n", cfile->file, cfile->line, arg );
-		cfile->err = 1;
-	}
+	if (cfile->rest && (arg = get_arg( cfile, ARG_OPTIONAL, NULL )))
+		conf_error( cfile, "excess token '%s'\n", arg );
 	while (fgets( cfile->buf, cfile->bufl, cfile->fp )) {
 		cfile->line++;
 		cfile->rest = cfile->buf;
@@ -546,9 +552,7 @@ load_config( const char *where )
 					cfile.ms_warn = 1;
 				  linkst:
 					if (*cfile.val != ':' || !(p = strchr( cfile.val + 1, ':' ))) {
-						error( "%s:%d: malformed mailbox spec\n",
-						       cfile.file, cfile.line );
-						cfile.err = 1;
+						conf_error( &cfile, "malformed mailbox spec\n" );
 						continue;
 					}
 					*p = 0;
@@ -559,18 +563,14 @@ load_config( const char *where )
 						}
 					}
 					channel->stores[fn] = (void *)~0;
-					error( "%s:%d: unknown store '%s'\n",
-					       cfile.file, cfile.line, cfile.val + 1 );
-					cfile.err = 1;
+					conf_error( &cfile, "unknown store '%s'\n", cfile.val + 1 );
 					continue;
 				  stpcom:
 					if (*++p)
 						channel->boxes[fn] = nfstrdup( p );
 				} else if (!getopt_helper( &cfile, &cops, channel )) {
-					error( "%s:%d: keyword '%s' is not recognized in Channel sections\n",
-					       cfile.file, cfile.line, cfile.cmd );
+					conf_error( &cfile, "keyword '%s' is not recognized in Channel sections\n", cfile.cmd );
 					cfile.rest = NULL;
-					cfile.err = 1;
 				}
 			}
 			if (!channel->stores[F]) {
@@ -615,10 +615,8 @@ load_config( const char *where )
 					arg = cfile.val;
 					goto addone;
 				} else {
-					error( "%s:%d: keyword '%s' is not recognized in Group sections\n",
-					       cfile.file, cfile.line, cfile.cmd );
+					conf_error( &cfile, "keyword '%s' is not recognized in Group sections\n", cfile.cmd );
 					cfile.rest = NULL;
-					cfile.err = 1;
 				}
 			}
 			glob_ok = 0;
@@ -627,36 +625,26 @@ load_config( const char *where )
 			UseFSync = parse_bool( &cfile );
 		} else if (!strcasecmp( "FieldDelimiter", cfile.cmd )) {
 			if (strlen( cfile.val ) != 1) {
-				error( "%s:%d: Field delimiter must be exactly one character long\n", cfile.file, cfile.line );
-				cfile.err = 1;
+				conf_error( &cfile, "Field delimiter must be exactly one character long\n" );
 			} else {
 				FieldDelimiter = cfile.val[0];
-				if (!ispunct( FieldDelimiter )) {
-					error( "%s:%d: Field delimiter must be a punctuation character\n", cfile.file, cfile.line );
-					cfile.err = 1;
-				}
+				if (!ispunct( FieldDelimiter ))
+					conf_error( &cfile, "Field delimiter must be a punctuation character\n" );
 			}
 		} else if (!strcasecmp( "BufferLimit", cfile.cmd )) {
 			BufferLimit = parse_size( &cfile );
-			if (!BufferLimit) {
-				error( "%s:%d: BufferLimit cannot be zero\n", cfile.file, cfile.line );
-				cfile.err = 1;
-			}
+			if (!BufferLimit)
+				conf_error( &cfile, "BufferLimit cannot be zero\n" );
 		} else if (!getopt_helper( &cfile, &gcops, &global_conf )) {
-			error( "%s:%d: '%s' is not a recognized section-starting or global keyword\n",
-			       cfile.file, cfile.line, cfile.cmd );
-			cfile.err = 1;
+			conf_error( &cfile, "'%s' is not a recognized section-starting or global keyword\n", cfile.cmd );
 			cfile.rest = NULL;
 			while (getcline( &cfile ))
 				if (!cfile.cmd)
 					goto reloop;
 			break;
 		}
-		if (!glob_ok) {
-			error( "%s:%d: global options may not follow sections\n",
-			       cfile.file, cfile.line );
-			cfile.err = 1;
-		}
+		if (!glob_ok)
+			conf_error( &cfile, "global options may not follow sections\n" );
 	}
 	fclose (cfile.fp);
 	if (cfile.ms_warn)
diff --git a/src/config.h b/src/config.h
index 0762b58..2177fb4 100644
--- a/src/config.h
+++ b/src/config.h
@@ -27,6 +27,9 @@ extern char FieldDelimiter;
 #define ARG_OPTIONAL 0
 #define ARG_REQUIRED 1
 
+void ATTR_PRINTFLIKE(2, 3) conf_error( conffile_t *cfile, const char *fmt, ... );
+void ATTR_PRINTFLIKE(2, 3) conf_sys_error( conffile_t *cfile, const char *fmt, ... );
+
 char *expand_strdup( const char *s, const conffile_t *cfile );
 
 char *get_arg( conffile_t *cfile, int required, int *comment );
diff --git a/src/driver.c b/src/driver.c
index 6b88dbb..885d7a3 100644
--- a/src/driver.c
+++ b/src/driver.c
@@ -83,15 +83,13 @@ parse_generic_store( store_conf_t *store, conffile_t *cfg, const char *type )
 		const char *p;
 		for (p = cfg->val; *p; p++) {
 			if (*p == '/') {
-				error( "%s:%d: flattened hierarchy delimiter cannot contain the canonical delimiter '/'\n", cfg->file, cfg->line );
-				cfg->err = 1;
+				conf_error( cfg, "flattened hierarchy delimiter cannot contain the canonical delimiter '/'\n" );
 				return;
 			}
 		}
 		store->flat_delim = nfstrdup( cfg->val );
 	} else {
-		error( "%s:%d: keyword '%s' is not recognized in %s sections\n", cfg->file, cfg->line, cfg->cmd, type );
+		conf_error( cfg, "keyword '%s' is not recognized in %s sections\n", cfg->cmd, type );
 		cfg->rest = NULL;
-		cfg->err = 1;
 	}
 }
diff --git a/src/drv_imap.c b/src/drv_imap.c
index ad95e3d..7f37fe5 100644
--- a/src/drv_imap.c
+++ b/src/drv_imap.c
@@ -3781,19 +3781,15 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 #endif
 		} else if (!strcasecmp( "Port", cfg->cmd )) {
 			int port = parse_int( cfg );
-			if ((unsigned)port > 0xffff) {
-				error( "%s:%d: Invalid port number\n", cfg->file, cfg->line );
-				cfg->err = 1;
-			} else {
+			if ((unsigned)port > 0xffff)
+				conf_error( cfg, "Invalid port number\n" );
+			else
 				server->sconf.port = (ushort)port;
-			}
 		} else if (!strcasecmp( "Timeout", cfg->cmd )) {
 			server->sconf.timeout = parse_int( cfg ) * 1000;
 		} else if (!strcasecmp( "PipelineDepth", cfg->cmd )) {
-			if ((server->max_in_progress = parse_int( cfg )) < 1) {
-				error( "%s:%d: PipelineDepth must be at least 1\n", cfg->file, cfg->line );
-				cfg->err = 1;
-			}
+			if ((server->max_in_progress = parse_int( cfg )) < 1)
+				conf_error( cfg, "PipelineDepth must be at least 1\n" );
 		} else if (!strcasecmp( "DisableExtension", cfg->cmd ) ||
 		           !strcasecmp( "DisableExtensions", cfg->cmd )) {
 			arg = cfg->val;
@@ -3804,34 +3800,27 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 						goto gotcap;
 					}
 				}
-				error( "%s:%d: Unrecognized IMAP extension '%s'\n", cfg->file, cfg->line, arg );
-				cfg->err = 1;
+				conf_error( cfg, "Unrecognized IMAP extension '%s'\n", arg );
 			  gotcap: ;
 			} while ((arg = get_arg( cfg, ARG_OPTIONAL, NULL )));
 #ifdef HAVE_LIBSSL
 		} else if (!strcasecmp( "CertificateFile", cfg->cmd )) {
 			server->sconf.cert_file = expand_strdup( cfg->val, cfg );
-			if (access( server->sconf.cert_file, R_OK )) {
-				sys_error( "%s:%d: CertificateFile '%s'",
-				           cfg->file, cfg->line, server->sconf.cert_file );
-				cfg->err = 1;
-			}
+			if (access( server->sconf.cert_file, R_OK ))
+				conf_sys_error( cfg, "CertificateFile '%s'",
+				                server->sconf.cert_file );
 		} else if (!strcasecmp( "SystemCertificates", cfg->cmd )) {
 			server->sconf.system_certs = parse_bool( cfg );
 		} else if (!strcasecmp( "ClientCertificate", cfg->cmd )) {
 			server->sconf.client_certfile = expand_strdup( cfg->val, cfg );
-			if (access( server->sconf.client_certfile, R_OK )) {
-				sys_error( "%s:%d: ClientCertificate '%s'",
-				           cfg->file, cfg->line, server->sconf.client_certfile );
-				cfg->err = 1;
-			}
+			if (access( server->sconf.client_certfile, R_OK ))
+				conf_sys_error( cfg, "ClientCertificate '%s'",
+				                server->sconf.client_certfile );
 		} else if (!strcasecmp( "ClientKey", cfg->cmd )) {
 			server->sconf.client_keyfile = expand_strdup( cfg->val, cfg );
-			if (access( server->sconf.client_keyfile, R_OK )) {
-				sys_error( "%s:%d: ClientKey '%s'",
-				           cfg->file, cfg->line, server->sconf.client_keyfile );
-				cfg->err = 1;
-			}
+			if (access( server->sconf.client_keyfile, R_OK ))
+				conf_sys_error( cfg, "ClientKey '%s'",
+				                server->sconf.client_keyfile );
 		} else if (!strcasecmp( "CipherString", cfg->cmd )) {
 			server->sconf.cipher_string = nfstrdup( cfg->val );
 		} else if (!strcasecmp( "TLSVersions", cfg->cmd )) {
@@ -3843,8 +3832,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 				} else if (*arg == '-') {
 					and_mask = ~0;
 				} else {
-					error( "%s:%d: TLSVersions arguments must start with +/-\n", cfg->file, cfg->line );
-					cfg->err = 1;
+					conf_error( cfg, "TLSVersions arguments must start with +/-\n" );
 					continue;
 				}
 				arg++;
@@ -3857,8 +3845,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 				} else if (!strcmp( "1.3", arg )) {
 					val = TLSv1_3;
 				} else {
-					error( "%s:%d: Unrecognized TLS version '%s'\n", cfg->file, cfg->line, arg );
-					cfg->err = 1;
+					conf_error( cfg, "Unrecognized TLS version '%s'\n", arg );
 					continue;
 				}
 				or_mask &= val;
@@ -3875,22 +3862,20 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 			server->sconf.ssl_versions = 0;
 			arg = cfg->val;
 			do {
-				if (!strcasecmp( "SSLv2", arg )) {
+				if (!strcasecmp( "SSLv2", arg ))
 					warn( "Warning: SSLVersion SSLv2 is no longer supported\n" );
-				} else if (!strcasecmp( "SSLv3", arg )) {
+				else if (!strcasecmp( "SSLv3", arg ))
 					warn( "Warning: SSLVersion SSLv3 is no longer supported\n" );
-				} else if (!strcasecmp( "TLSv1", arg )) {
+				else if (!strcasecmp( "TLSv1", arg ))
 					server->sconf.ssl_versions |= TLSv1;
-				} else if (!strcasecmp( "TLSv1.1", arg )) {
+				else if (!strcasecmp( "TLSv1.1", arg ))
 					server->sconf.ssl_versions |= TLSv1_1;
-				} else if (!strcasecmp( "TLSv1.2", arg )) {
+				else if (!strcasecmp( "TLSv1.2", arg ))
 					server->sconf.ssl_versions |= TLSv1_2;
-				} else if (!strcasecmp( "TLSv1.3", arg )) {
+				else if (!strcasecmp( "TLSv1.3", arg ))
 					server->sconf.ssl_versions |= TLSv1_3;
-				} else {
-					error( "%s:%d: Unrecognized SSL version\n", cfg->file, cfg->line );
-					cfg->err = 1;
-				}
+				else
+					conf_error( cfg, "Unrecognized SSL version\n" );
 			} while ((arg = get_arg( cfg, ARG_OPTIONAL, NULL )));
 #else
 		} else if (!strcasecmp( "CertificateFile", cfg->cmd ) ||
@@ -3927,8 +3912,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 				cfg->err = 1;
 #endif
 			} else {
-				error( "%s:%d: Invalid TLS type\n", cfg->file, cfg->line );
-				cfg->err = 1;
+				conf_error( cfg, "Invalid TLS type\n" );
 			}
 		} else if (!strcasecmp( "AuthMech", cfg->cmd ) ||
 		         !strcasecmp( "AuthMechs", cfg->cmd )) {
@@ -3946,8 +3930,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 					if (srv->name && !strcmp( srv->name, cfg->val ))
 						goto gotsrv;
 				store->server = (void *)~0;
-				error( "%s:%d: unknown IMAP account '%s'\n", cfg->file, cfg->line, cfg->val );
-				cfg->err = 1;
+				conf_error( cfg, "unknown IMAP account '%s'\n", cfg->val );
 				continue;
 			  gotsrv:
 				store->server = srv;
@@ -3959,8 +3942,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 				store->path = nfstrdup( cfg->val );
 			} else if (!strcasecmp( "PathDelimiter", cfg->cmd )) {
 				if (strlen( cfg->val ) != 1) {
-					error( "%s:%d: Path delimiter must be exactly one character long\n", cfg->file, cfg->line );
-					cfg->err = 1;
+					conf_error( cfg, "Path delimiter must be exactly one character long\n" );
 					continue;
 				}
 				store->delimiter = cfg->val[0];
@@ -3969,10 +3951,8 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 			}
 			continue;
 		} else {
-			error( "%s:%d: keyword '%s' is not recognized in IMAPAccount sections\n",
-			       cfg->file, cfg->line, cfg->cmd );
+			conf_error( cfg, "keyword '%s' is not recognized in IMAPAccount sections\n", cfg->cmd );
 			cfg->rest = NULL;
-			cfg->err = 1;
 			continue;
 		}
 		acc_opt = 1;
diff --git a/src/drv_maildir.c b/src/drv_maildir.c
index cdffc32..05b3db4 100644
--- a/src/drv_maildir.c
+++ b/src/drv_maildir.c
@@ -1899,27 +1899,23 @@ maildir_parse_store( conffile_t *cfg, store_conf_t **storep )
 #endif /* USE_DB */
 		} else if (!strcasecmp( "InfoDelimiter", cfg->cmd )) {
 			if (strlen( cfg->val ) != 1) {
-				error( "%s:%d: Info delimiter must be exactly one character long\n", cfg->file, cfg->line );
-				cfg->err = 1;
+				conf_error( cfg, "Info delimiter must be exactly one character long\n" );
 				continue;
 			}
 			store->info_delimiter = cfg->val[0];
 			if (!ispunct( store->info_delimiter )) {
-				error( "%s:%d: Info delimiter must be a punctuation character\n", cfg->file, cfg->line );
-				cfg->err = 1;
+				conf_error( cfg, "Info delimiter must be a punctuation character\n" );
 				continue;
 			}
 		} else if (!strcasecmp( "SubFolders", cfg->cmd )) {
-			if (!strcasecmp( "Verbatim", cfg->val )) {
+			if (!strcasecmp( "Verbatim", cfg->val ))
 				store->sub_style = SUB_VERBATIM;
-			} else if (!strcasecmp( "Maildir++", cfg->val )) {
+			else if (!strcasecmp( "Maildir++", cfg->val ))
 				store->sub_style = SUB_MAILDIRPP;
-			} else if (!strcasecmp( "Legacy", cfg->val )) {
+			else if (!strcasecmp( "Legacy", cfg->val ))
 				store->sub_style = SUB_LEGACY;
-			} else {
-				error( "%s:%d: Unrecognized SubFolders style\n", cfg->file, cfg->line );
-				cfg->err = 1;
-			}
+			else
+				conf_error( cfg, "Unrecognized SubFolders style\n" );
 		} else {
 			parse_generic_store( &store->gen, cfg, "MaildirStore" );
 		}
-- 
2.41.0

>From fdc1dc5e2e908918fcd486674091b64b77faa2de Mon Sep 17 00:00:00 2001
From: Michiel van den Heuvel <michielvdnheu...@gmail.com>
Date: Thu, 17 Aug 2023 20:25:51 +0200
Subject: [PATCH 2/2] Add IncludeCmd directive to config parser

---
 src/config.c | 72 +++++++++++++++++++++++++++++++++++++++++++++++-----
 src/config.h |  3 +++
 src/mbsync.1 |  8 ++++++
 3 files changed, 77 insertions(+), 6 deletions(-)

diff --git a/src/config.c b/src/config.c
index 2995458..0cbf607 100644
--- a/src/config.c
+++ b/src/config.c
@@ -61,13 +61,21 @@ expand_strdup( const char *s, const conffile_t *cfile )
 	}
 }
 
+static void
+conf_print_loc( const conffile_t *cfile )
+{
+	if (cfile->include_fp)
+		fprintf( stderr, "%s:%d:included:%d: ", cfile->file, cfile->line, cfile->include_line );
+	else
+		fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
+}
+
 void
 conf_error( conffile_t *cfile, const char *fmt, ... )
 {
 	va_list va;
 
-	flushn();
-	fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
+	conf_print_loc( cfile );
 	va_start( va, fmt );
 	vfprintf( stderr, fmt, va );
 	va_end( va );
@@ -79,7 +87,7 @@ conf_sys_error( conffile_t *cfile, const char *fmt, ... )
 {
 	va_list va;
 
-	fprintf( stderr, "%s:%d: ", cfile->file, cfile->line );
+	conf_print_loc( cfile );
 	va_start( va, fmt );
 	vsys_error( fmt, va );
 	va_end( va );
@@ -317,6 +325,50 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 	return 1;
 }
 
+static void
+include_cmd_popen( conffile_t *cfile, const char *cmd )
+{
+	if (!(cfile->include_fp = popen( cmd, "r" ))) {
+		sys_error( "popen" );
+		cfile->err = 1;
+		return;
+	}
+	cfile->include_line = 0;
+	cfile->include_command = nfstrdup( cmd );
+}
+
+static void
+include_cmd_pclose( conffile_t *cfile )
+{
+	int ret;
+
+	if ((ret = pclose( cfile->include_fp ))) {
+		if (ret < 0)
+			conf_sys_error( cfile, "pclose" );
+		else if (WIFSIGNALED( ret ))
+			conf_error( cfile, "command \"%s\" crashed with signal %d\n",
+			            cfile->include_command, WTERMSIG( ret ) );
+		else
+			conf_error( cfile, "command \"%s\" exited with status %d\n",
+			            cfile->include_command, WEXITSTATUS( ret ) );
+	}
+	cfile->include_fp = NULL;
+	free( cfile->include_command );
+}
+
+static int
+read_cline( conffile_t *cfile )
+{
+	if (cfile->include_fp) {
+		cfile->include_line++;
+		if ((cfile->rest = fgets( cfile->buf, cfile->bufl, cfile->include_fp )))
+			return 1;
+		include_cmd_pclose( cfile );
+	}
+	cfile->line++;
+	return (cfile->rest = fgets( cfile->buf, cfile->bufl, cfile->fp )) != NULL;
+}
+
 int
 getcline( conffile_t *cfile )
 {
@@ -325,9 +377,13 @@ getcline( conffile_t *cfile )
 
 	if (cfile->rest && (arg = get_arg( cfile, ARG_OPTIONAL, NULL )))
 		conf_error( cfile, "excess token '%s'\n", arg );
-	while (fgets( cfile->buf, cfile->bufl, cfile->fp )) {
-		cfile->line++;
-		cfile->rest = cfile->buf;
+	if (cfile->cmd && !strcasecmp( cfile->cmd, "IncludeCmd" )) {
+		if (cfile->include_fp)
+			conf_error( cfile, "nested IncludeCmd\n" );
+		else
+			include_cmd_popen( cfile, cfile->val );
+	}
+	while (read_cline( cfile )) {
 		if (!(cfile->cmd = get_arg( cfile, ARG_OPTIONAL, &comment ))) {
 			if (comment)
 				continue;
@@ -335,6 +391,8 @@ getcline( conffile_t *cfile )
 		}
 		if (!(cfile->val = get_arg( cfile, ARG_REQUIRED, NULL )))
 			continue;
+		if (!strcasecmp( cfile->cmd, "IncludeCmd" ))
+			return getcline( cfile );
 		return 1;
 	}
 	return 0;
@@ -487,6 +545,7 @@ load_config( const char *where )
 		return 1;
 	}
 	buf[sizeof(buf) - 1] = 0;
+	cfile.include_fp = NULL;
 	cfile.buf = buf;
 	cfile.bufl = sizeof(buf) - 1;
 	cfile.line = 0;
@@ -494,6 +553,7 @@ load_config( const char *where )
 	cfile.ms_warn = 0;
 	cfile.renew_warn = 0;
 	cfile.delete_warn = 0;
+	cfile.cmd = NULL;
 	cfile.rest = NULL;
 
 	gcops = 0;
diff --git a/src/config.h b/src/config.h
index 2177fb4..694178b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -12,10 +12,13 @@
 
 typedef struct {
 	const char *file;
+	char *include_command;
 	FILE *fp;
+	FILE *include_fp;
 	char *buf;
 	int bufl;
 	int line;
+	int include_line;
 	int err;
 	int ms_warn, renew_warn, delete_warn;
 	int path_len;
diff --git a/src/mbsync.1 b/src/mbsync.1
index 939c8c5..1a9e70e 100644
--- a/src/mbsync.1
+++ b/src/mbsync.1
@@ -786,6 +786,14 @@ absolute limit, as even a single message can consume more memory than
 this.
 (Default: \fI10M\fR)
 .
+.SS Dynamic Configuration
+.TP
+\fBIncludeCmd\fR \fIcommand\fR
+Specify a shell command to obtain configuration file content from.
+This keyword can appear anywhere. The command's output will be
+interpreted as if it appeared inline; it can range from nothing at all
+to multiple sections.
+.
 .SH CONSOLE OUTPUT
 If \fBmbsync\fR's output is connected to a console, it will print progress
 counters by default. The output will look like this:
-- 
2.41.0

_______________________________________________
isync-devel mailing list
isync-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/isync-devel

Reply via email to