Hi Oswald,

> > It might be worthwhile to set `cfile->error` here too, and
> > I'll gladly amend the patch if you agree.
>
> yeah, i'd do that.

Done!

> theoretically yes, though in practice i'd recommend that users just call 
> a script, because the extra layer of quoting/escaping makes things ugly 
> and easy to get wrong. add it if you can't help yourself. ;)

> so just call a script already! ;)

Oh you should see my personal configuration. So many inline scripts
under multiple layers of escaping. But, with my own troubles in getting
a clean implementation plus the feedback, I think I can help myself ;) 

External scripts it is.

> eval => included, i guess.
> also, that space afterwards should be probably a colon, otherwise it's
> kinda confusing and weird-looking.

Changed it, but are you sure? Now that the command is called `Eval`? The
space *was* weird-looking, thanks. I was already unhappy about it.

> on that matter, have you tested all error scenarios?

The ones I added? Or all that were modified? 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.

> >     +void
> >     +eval_popen( conffile_t *cfile, const char *cmd )
> 
> should return an int (zero is error), which should lead to early 
> termination.

It was missing the `cfile->err` plumbing. Which is fixed, assuming that's
what you meant, in the attached implementation. I could also return 0 from
getcline in that case, but that does not match the rest of the parser. Is
a failed popen that different?

> ... but here it gets "interesting":
> seeing the command after dequoting may be actually helpful, especially 
> when multi-line commands are supported. though of course this can be 
> ridiculously long. but i guess we'll get away with just qouting it.

Added this.

> >     +char *
> >     +read_cline( conffile_t *cfile )
>
> i think that should be just an int.

Fixed (?) by comparing it to `NULL` when returning. Also made it less
convoluted.

> >    +.SS Dynamic options
>
> why a separate section? it doesn't really fit the pattern for the *Cmd 
> commands.

I didn't really know where to put it. It can occur literally everywhere,
which no other options (like the `*Cmd`) do, which is why I did not add
it there. This time I put it next to the global options.

> >+void
> >+conf_error( const conffile_t *cfile, const char* fmt, ... )
>
> second asterisk has space on the wrong side.
> this repeats for several other string arguments.
> but i think this is actually the only whitespace mistake you've made, 
> which may be a first among non-trivial external isync contributions 
> during my "reign". ^^

Hehehe :) Nice trophy. I don't suppose you have some formatter config
file?

(Hopefully) All your other proposed changes were incorporated in these
revised patches.

Cheers!
>From 92cc071aeb14cab302eb0db206a30e2c5c78623b 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      | 106 +++++++++++++++++++++-------------------------
 src/config.h      |   3 ++
 src/driver.c      |   6 +--
 src/drv_imap.c    |  46 +++++++-------------
 src/drv_maildir.c |   9 ++--
 5 files changed, 72 insertions(+), 98 deletions(-)

diff --git a/src/config.c b/src/config.c
index 456bd47..0de2cb2 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 )
 {
@@ -76,8 +101,7 @@ get_arg( conffile_t *cfile, int required, int *comment )
 		if (comment)
 			*comment = (c == '#');
 		if (required) {
-			error( "%s:%d: parameter missing\n", cfile->file, cfile->line );
-			cfile->err = 1;
+			conf_error( cfile, "parameter missing\n" );
 		}
 		ret = NULL;
 	} else {
@@ -98,13 +122,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 +146,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 +159,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 +179,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 +265,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;
@@ -268,9 +282,7 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 		} 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;
+			conf_error( cfile, "invalid ExpireSide argument '%s'\n", arg );
 		}
 	} else if (!strcasecmp( "ExpireUnread", cfile->cmd )) {
 		conf->expire_unread = parse_bool( cfile );
@@ -295,9 +307,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 +325,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 +554,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 +565,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 +617,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 +627,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..cbce8f4 100644
--- a/src/config.h
+++ b/src/config.h
@@ -29,6 +29,9 @@ extern char FieldDelimiter;
 
 char *expand_strdup( const char *s, const conffile_t *cfile );
 
+void ATTR_PRINTFLIKE(2, 3) conf_error( conffile_t *, const char *, ... );
+void ATTR_PRINTFLIKE(2, 3) conf_sys_error( conffile_t *, const char *, ... );
+
 char *get_arg( conffile_t *cfile, int required, int *comment );
 
 char parse_bool( conffile_t *cfile );
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..a0b2305 100644
--- a/src/drv_imap.c
+++ b/src/drv_imap.c
@@ -3782,8 +3782,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 		} 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;
+				conf_error( cfg, "Invalid port number\n" );
 			} else {
 				server->sconf.port = (ushort)port;
 			}
@@ -3791,8 +3790,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 			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;
+				conf_error( cfg, "PipelineDepth must be at least 1\n" );
 			}
 		} else if (!strcasecmp( "DisableExtension", cfg->cmd ) ||
 		           !strcasecmp( "DisableExtensions", cfg->cmd )) {
@@ -3804,33 +3802,29 @@ 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;
+				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;
+				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;
+				conf_sys_error( cfg, "ClientKey '%s'",
+				                server->sconf.client_keyfile );
 			}
 		} else if (!strcasecmp( "CipherString", cfg->cmd )) {
 			server->sconf.cipher_string = nfstrdup( cfg->val );
@@ -3843,8 +3837,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 +3850,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;
@@ -3888,8 +3880,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep )
 				} 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;
+					conf_error( cfg, "Unrecognized SSL version\n" );
 				}
 			} while ((arg = get_arg( cfg, ARG_OPTIONAL, NULL )));
 #else
@@ -3927,8 +3918,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 +3936,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 +3948,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 +3957,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..3ce9cce 100644
--- a/src/drv_maildir.c
+++ b/src/drv_maildir.c
@@ -1899,14 +1899,12 @@ 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 )) {
@@ -1917,8 +1915,7 @@ maildir_parse_store( conffile_t *cfg, store_conf_t **storep )
 			} 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;
+				conf_error( cfg, "Unrecognized SubFolders style\n" );
 			}
 		} else {
 			parse_generic_store( &store->gen, cfg, "MaildirStore" );
-- 
2.41.0

>From 5f1f3ebd8192c7d325cf7fa8fb50914ec0fe423b 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 Eval directive to config parser

---
 src/config.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++---
 src/config.h |  3 +++
 src/mbsync.1 |  6 +++++
 3 files changed, 76 insertions(+), 4 deletions(-)

diff --git a/src/config.c b/src/config.c
index 0de2cb2..09f1fec 100644
--- a/src/config.c
+++ b/src/config.c
@@ -61,13 +61,22 @@ expand_strdup( const char *s, const conffile_t *cfile )
 	}
 }
 
+static void
+conf_print_loc( const conffile_t *cfile )
+{
+	if (cfile->eval_fp)
+		fprintf( stderr, "%s:%d:included:%d: ", cfile->file, cfile->line, cfile->eval_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 +88,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 );
@@ -319,6 +328,50 @@ getopt_helper( conffile_t *cfile, int *cops, channel_conf_t *conf )
 	return 1;
 }
 
+static void
+eval_popen( conffile_t *cfile, const char *cmd )
+{
+	if (!(cfile->eval_fp = popen( cmd, "r" ))) {
+		sys_error( "popen failed" );
+		cfile->err = 1;
+	}
+	cfile->eval_line = 0;
+	cfile->eval_command = nfstrdup( cmd );
+}
+
+static void
+eval_pclose( conffile_t *cfile )
+{
+	int ret;
+
+	ret = pclose( cfile->eval_fp );
+	cfile->eval_fp = NULL;
+	if (ret) {
+		if (ret < 0)
+			conf_sys_error( cfile, "command failed: %s", cfile->eval_command );
+		else if (WIFSIGNALED( ret ))
+			conf_error( cfile, "command crashed: %s\n", cfile->eval_command );
+		else
+			conf_error( cfile, "command exited with status %d: %s\n",
+			            WEXITSTATUS( ret ), cfile->eval_command );
+	}
+	free( cfile->eval_command );
+}
+
+static int
+read_cline( conffile_t *cfile )
+{
+	if (cfile->eval_fp) {
+		cfile->eval_line++;
+		if (fgets( cfile->buf, cfile->bufl, cfile->eval_fp ))
+			return 1;
+		else
+			eval_pclose( cfile );
+	}
+	cfile->line++;
+	return fgets( cfile->buf, cfile->bufl, cfile->fp ) != NULL;
+}
+
 int
 getcline( conffile_t *cfile )
 {
@@ -327,14 +380,23 @@ 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++;
+	while (read_cline( cfile )) {
 		cfile->rest = cfile->buf;
 		if (!(cfile->cmd = get_arg( cfile, ARG_OPTIONAL, &comment ))) {
 			if (comment)
 				continue;
 			return 1;
 		}
+		if (!strcmp( cfile->cmd, "Eval" )) {
+			if ((arg = get_arg( cfile, ARG_REQUIRED, NULL ))) {
+				if (cfile->eval_fp) {
+					conf_error( cfile, "nested eval\n" );
+					continue;
+				}
+				eval_popen( cfile, arg );
+			}
+			continue;
+		}
 		if (!(cfile->val = get_arg( cfile, ARG_REQUIRED, NULL )))
 			continue;
 		return 1;
@@ -489,6 +551,7 @@ load_config( const char *where )
 		return 1;
 	}
 	buf[sizeof(buf) - 1] = 0;
+	cfile.eval_fp = NULL;
 	cfile.buf = buf;
 	cfile.bufl = sizeof(buf) - 1;
 	cfile.line = 0;
diff --git a/src/config.h b/src/config.h
index cbce8f4..be08a5c 100644
--- a/src/config.h
+++ b/src/config.h
@@ -12,10 +12,13 @@
 
 typedef struct {
 	const char *file;
+	char *eval_command;
 	FILE *fp;
+	FILE *eval_fp;
 	char *buf;
 	int bufl;
 	int line;
+	int eval_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..f32218b 100644
--- a/src/mbsync.1
+++ b/src/mbsync.1
@@ -786,6 +786,12 @@ absolute limit, as even a single message can consume more memory than
 this.
 (Default: \fI10M\fR)
 .
+.TP
+\fBEval\fR \fIcommand\fR
+Specify a shell command to obtain options. The command can output zero
+or more lines which will be interpreted as if they appeared inline. This
+keyword is allowed in every section.
+.
 .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