Repository: favonia/cloudflare-ddns
Pull request #415 (2023-03-11 09:32:33)

fix: make the quiet mode quieter
The single-run mode enabled by #411 (requested by #409) made it obvious that the quiet mode should be quieter. A few areas to improve:

- [x] updater version printing
- [x] privilege dropping
- [x] "Bye!"
- [x] config errors other than parsing templates
- [x] parsing errors

codecov[bot]: ## [Codecov](https://codecov.io/gh/favonia/cloudflare-ddns/pull/415?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
> Merging [#415](https://codecov.io/gh/favonia/cloudflare-ddns/pull/415?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (10893c2) into [main](https://codecov.io/gh/favonia/cloudflare-ddns/commit/5bae63751c1a2e6a7dd77b724897249f324c9889?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) (5bae637) will **increase** coverage by `0.12%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##             main     #415      +/-   ##
==========================================
+ Coverage   87.71%   87.83%   +0.12%     
==========================================
  Files          33       33              
  Lines        2075     2072       -3     
==========================================
  Hits         1820     1820              
+ Misses        243      240       -3     
  Partials       12       12              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `87.83% <0.00%> (+0.12%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/favonia/cloudflare-ddns/pull/415?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [cmd/ddns/ddns.go](https://codecov.io/gh/favonia/cloudflare-ddns/pull/415?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMvZGRucy5nbw==) | `0.00% <0.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia)


Patch:
diff --git a/README.markdown b/README.markdown
index f67e78c..4be180e 100644
--- a/README.markdown
+++ b/README.markdown
@@ -101,9 +101,9 @@ services:
     # Restart the updater after reboot
     cap_add:
       - SETUID
-        # Capability to change UID
+        # Capability to change user ID
       - SETGID
-        # Capability to change GID
+        # Capability to change group ID
     cap_drop:
       - all
       # Drop all other capabilities
diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go
index 630f674..424fc98 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -55,7 +55,6 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter
 
 func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) {
 	if c.DeleteOnStop {
-		ppfmt.Noticef(pp.EmojiClearRecord, "Deleting all managed records . . .")
 		if ok, msg := updater.ClearIPs(ctx, ppfmt, c, s); ok {
 			c.Monitor.Log(ctx, ppfmt, msg)
 		} else {
@@ -71,19 +70,18 @@ func main() {
 func realMain() int { //nolint:funlen
 	ppfmt := pp.New(os.Stdout)
 	if !config.ReadEmoji("EMOJI", &ppfmt) || !config.ReadQuiet("QUIET", &ppfmt) {
-		ppfmt.Noticef(pp.EmojiUserError, "Bye!")
+		ppfmt.Infof(pp.EmojiUserError, "Bye!")
 		return 1
 	}
-	if !ppfmt.IsEnabledFor(pp.Info) {
-		ppfmt.Noticef(pp.EmojiMute, "Quiet mode enabled")
-	}
 
 	// Show the name and the version of the updater
-	ppfmt.Noticef(pp.EmojiStar, formatName())
+	ppfmt.Infof(pp.EmojiStar, formatName())
 
 	// Drop the superuser privilege
-	droproot.DropPriviledges(ppfmt)
-	droproot.PrintPriviledges(ppfmt)
+	if !droproot.DropPrivileges(ppfmt) {
+		ppfmt.Infof(pp.EmojiBye, "Bye!")
+		return 1
+	}
 
 	// Catch signals SIGINT and SIGTERM
 	sig := signal.Setup()
@@ -99,7 +97,7 @@ func realMain() int { //nolint:funlen
 	// Bail out now if initConfig failed
 	if !configOk {
 		c.Monitor.ExitStatus(ctx, ppfmt, 1, "Config errors")
-		ppfmt.Noticef(pp.EmojiBye, "Bye!")
+		ppfmt.Infof(pp.EmojiBye, "Bye!")
 		return 1
 	}
 
@@ -123,7 +121,7 @@ func realMain() int { //nolint:funlen
 
 		// Maybe the cron was disabled?
 		if c.UpdateCron == nil {
-			ppfmt.Noticef(pp.EmojiBye, "Bye!")
+			ppfmt.Infof(pp.EmojiBye, "Bye!")
 			return 0
 		}
 
@@ -132,7 +130,7 @@ func realMain() int { //nolint:funlen
 			ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future")
 			stopUpdating(ctx, ppfmt, c, s)
 			c.Monitor.ExitStatus(ctx, ppfmt, 1, "No scheduled updates")
-			ppfmt.Noticef(pp.EmojiBye, "Bye!")
+			ppfmt.Infof(pp.EmojiBye, "Bye!")
 			return 1
 		}
 
@@ -144,7 +142,7 @@ func realMain() int { //nolint:funlen
 		if !sig.Sleep(ppfmt, interval) {
 			stopUpdating(ctx, ppfmt, c, s)
 			c.Monitor.ExitStatus(ctx, ppfmt, 0, "Terminated")
-			ppfmt.Noticef(pp.EmojiBye, "Bye!")
+			ppfmt.Infof(pp.EmojiBye, "Bye!")
 			return 0
 		}
 	} // mainLoop
diff --git a/go.mod b/go.mod
index 01e7453..2b0f2ae 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/text v0.8.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index 750a437..8fd1122 100644
--- a/go.sum
+++ b/go.sum
@@ -41,6 +41,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw=
+golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
diff --git a/internal/config/config.go b/internal/config/config.go
index 1228361..a0ad905 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -6,6 +6,8 @@ import (
 	"strings"
 	"time"
 
+	"golang.org/x/exp/slices"
+
 	"github.com/favonia/cloudflare-ddns/internal/api"
 	"github.com/favonia/cloudflare-ddns/internal/cron"
 	"github.com/favonia/cloudflare-ddns/internal/domain"
@@ -114,25 +116,7 @@ func ReadAuth(ppfmt pp.PP, field *api.Auth) bool {
 // returning true if elements are already distinct.
 func deduplicate(list []domain.Domain) []domain.Domain {
 	domain.SortDomains(list)
-
-	if len(list) == 0 {
-		return list
-	}
-
-	j := 0
-	for i := range list {
-		if i == 0 || list[j] == list[i] {
-			continue
-		}
-		j++
-		list[j] = list[i]
-	}
-
-	if len(list) == j+1 {
-		return list
-	}
-
-	return list[:j+1]
+	return slices.Compact(list)
 }
 
 // ReadDomainMap reads environment variables DOMAINS, IP4_DOMAINS, and IP6_DOMAINS
@@ -363,7 +347,7 @@ func (c *Config) NormalizeConfig(ppfmt pp.PP) bool {
 	}
 
 	// fill in proxyMap
-	proxiedPred, ok := domainexp.ParseExpression(ppfmt, c.ProxiedTemplate)
+	proxiedPred, ok := domainexp.ParseExpression(ppfmt, "PROXIED", c.ProxiedTemplate)
 	if !ok {
 		return false
 	}
diff --git a/internal/config/env.go b/internal/config/env.go
index 6612fe0..ed5c3f1 100644
--- a/internal/config/env.go
+++ b/internal/config/env.go
@@ -34,14 +34,14 @@ func ReadString(ppfmt pp.PP, key string, field *string) bool {
 
 // ReadEmoji reads an environment variable as emoji/no-emoji.
 func ReadEmoji(key string, ppfmt *pp.PP) bool {
-	val := Getenv(key)
-	if val == "" {
+	valEmoji := Getenv(key)
+	if valEmoji == "" {
 		return true
 	}
 
-	emoji, err := strconv.ParseBool(val)
+	emoji, err := strconv.ParseBool(valEmoji)
 	if err != nil {
-		(*ppfmt).Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		(*ppfmt).Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, valEmoji, err)
 		return false
 	}
 
@@ -52,18 +52,18 @@ func ReadEmoji(key string, ppfmt *pp.PP) bool {
 
 // ReadQuiet reads an environment variable as quiet/verbose.
 func ReadQuiet(key string, ppfmt *pp.PP) bool {
-	val := Getenv(key)
-	if val == "" {
+	valQuiet := Getenv(key)
+	if valQuiet == "" {
 		return true
 	}
 
-	b, err := strconv.ParseBool(val)
+	quiet, err := strconv.ParseBool(valQuiet)
 	if err != nil {
-		(*ppfmt).Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		(*ppfmt).Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, valQuiet, err)
 		return false
 	}
 
-	if b {
+	if quiet {
 		*ppfmt = (*ppfmt).SetLevel(pp.Quiet)
 	} else {
 		*ppfmt = (*ppfmt).SetLevel(pp.Verbose)
@@ -82,7 +82,7 @@ func ReadBool(ppfmt pp.PP, key string, field *bool) bool {
 
 	b, err := strconv.ParseBool(val)
 	if err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, val, err)
 		return false
 	}
 
@@ -90,8 +90,8 @@ func ReadBool(ppfmt pp.PP, key string, field *bool) bool {
 	return true
 }
 
-// ReadNonnegInt reads an environment variable as an integer.
-func ReadNonnegInt(ppfmt pp.PP, key string, field *int) bool {
+// ReadLinuxID reads an environment variable as a user or group ID.
+func ReadLinuxID(ppfmt pp.PP, key string, field *int) bool {
 	val := Getenv(key)
 	if val == "" {
 		ppfmt.Infof(pp.EmojiBullet, "Use default %s=%d", key, *field)
@@ -101,15 +101,45 @@ func ReadNonnegInt(ppfmt pp.PP, key string, field *int) bool {
 	i, err := strconv.Atoi(val)
 	switch {
 	case err != nil:
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, val, err)
 		return false
+
 	case i < 0:
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %d is negative", val, i)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%d) is negative", key, i)
+		return false
+
+	case i == 0:
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%d) cannot be zero (the superuser)", key, i)
 		return false
+
+	default:
+		*field = i
+		return true
 	}
+}
 
-	*field = i
-	return true
+// ReadNonnegInt reads an environment variable as a non-negative integer.
+func ReadNonnegInt(ppfmt pp.PP, key string, field *int) bool {
+	val := Getenv(key)
+	if val == "" {
+		ppfmt.Infof(pp.EmojiBullet, "Use default %s=%d", key, *field)
+		return true
+	}
+
+	i, err := strconv.Atoi(val)
+	switch {
+	case err != nil:
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, val, err)
+		return false
+
+	case i < 0:
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%d) is negative", key, i)
+		return false
+
+	default:
+		*field = i
+		return true
+	}
 }
 
 // ReadTTL reads a valid TTL value.
@@ -130,21 +160,22 @@ func ReadTTL(ppfmt pp.PP, key string, field *api.TTL) bool {
 	res, err := strconv.Atoi(val)
 	switch {
 	case err != nil:
-		ppfmt.Errorf(pp.EmojiUserError, "TTL (%q) is not a number: %v", val, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, val, err)
 		return false
 
 	case res != 1 && (res < 30 || res > 86400):
-		ppfmt.Errorf(pp.EmojiUserError, "TTL (%d) should be 1 (auto) or between 30 and 86400", res)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%d) should be 1 (auto) or between 30 and 86400", key, res)
 		return false
-	}
 
-	*field = api.TTL(res)
-	return true
+	default:
+		*field = api.TTL(res)
+		return true
+	}
 }
 
 // ReadDomains reads an environment variable as a comma-separated list of domains.
 func ReadDomains(ppfmt pp.PP, key string, field *[]domain.Domain) bool {
-	if list, ok := domainexp.ParseList(ppfmt, Getenv(key)); ok {
+	if list, ok := domainexp.ParseList(ppfmt, key, Getenv(key)); ok {
 		*field = list
 		return true
 	}
@@ -166,7 +197,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "cloudflare":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s and provider "cloudflare" were deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+				`%s=cloudflare is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
 				keyDeprecated, key, key,
 			)
 			*field = provider.NewCloudflareTrace()
@@ -174,7 +205,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "cloudflare.trace":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s was deprecated; use %s=%s`,
+				`%s is deprecated; use %s=%s`,
 				keyDeprecated, key, valPolicy,
 			)
 			*field = provider.NewCloudflareTrace()
@@ -182,7 +213,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "cloudflare.doh":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s was deprecated; use %s=%s`,
+				`%s is deprecated; use %s=%s`,
 				keyDeprecated, key, valPolicy,
 			)
 			*field = provider.NewCloudflareDOH()
@@ -190,7 +221,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "ipify":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s and provider "ipify" were deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+				`%s=ipify is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
 				keyDeprecated, key, key,
 			)
 			*field = provider.NewIpify()
@@ -198,7 +229,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "local":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s was deprecated; use %s=%s`,
+				`%s is deprecated; use %s=%s`,
 				keyDeprecated, key, valPolicy,
 			)
 			*field = provider.NewLocal()
@@ -206,13 +237,13 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "unmanaged":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Parameter %s was deprecated; use %s=none`,
+				`%s is deprecated; use %s=none`,
 				keyDeprecated, key,
 			)
 			*field = nil
 			return true
 		default:
-			ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: not a valid provider", valPolicy)
+			ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a valid provider", keyDeprecated, valPolicy)
 			return false
 		}
 	} else {
@@ -229,7 +260,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "cloudflare":
 			ppfmt.Errorf(
 				pp.EmojiUserError,
-				`Parameter %s does not accept "cloudflare"; use %s=cloudflare.trace or %s=cloudflare.doh`,
+				`%s=cloudflare is invalid; use %s=cloudflare.trace or %s=cloudflare.doh`,
 				key, key, key,
 			)
 			return false
@@ -242,8 +273,8 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 		case "ipify":
 			ppfmt.Warningf(
 				pp.EmojiUserWarning,
-				`Provider "ipify" was deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
-				key, key,
+				`%s=ipify is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+				key, key, key,
 			)
 			*field = provider.NewIpify()
 			return true
@@ -254,7 +285,7 @@ func ReadProvider(ppfmt pp.PP, key, keyDeprecated string, field *provider.Provid
 			*field = nil
 			return true
 		default:
-			ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: not a valid provider", val)
+			ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a valid provider", key, val)
 			return false
 		}
 	}
@@ -272,10 +303,10 @@ func ReadNonnegDuration(ppfmt pp.PP, key string, field *time.Duration) bool {
 
 	switch {
 	case err != nil:
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a time duration: %v", key, val, err)
 		return false
 	case t < 0:
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v is negative", val, t)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%v) is negative", key, t)
 		return false
 	}
 
@@ -293,7 +324,7 @@ func ReadCron(ppfmt pp.PP, key string, field *cron.Schedule) bool {
 
 	c, err := cron.New(val)
 	if err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v", val, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a cron expression: %v", key, val, err)
 		return false
 	}
 
diff --git a/internal/domainexp/lexer.go b/internal/domainexp/lexer.go
index c4bcf4a..f743520 100644
--- a/internal/domainexp/lexer.go
+++ b/internal/domainexp/lexer.go
@@ -99,7 +99,7 @@ func splitter(data []byte, atEOF bool) (int, []byte, error) {
 	}
 }
 
-func tokenize(ppfmt pp.PP, input string) ([]string, bool) {
+func tokenize(ppfmt pp.PP, key string, input string) ([]string, bool) {
 	scanner := bufio.NewScanner(strings.NewReader(input))
 	scanner.Split(splitter)
 
@@ -110,7 +110,7 @@ func tokenize(ppfmt pp.PP, input string) ([]string, bool) {
 	}
 
 	if err := scanner.Err(); err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: %v", input, err)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, input, err)
 		return nil, false
 	}
 	return tokens, true
diff --git a/internal/domainexp/parser.go b/internal/domainexp/parser.go
index dd90ea5..a3046f3 100644
--- a/internal/domainexp/parser.go
+++ b/internal/domainexp/parser.go
@@ -8,7 +8,7 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-func scanList(ppfmt pp.PP, input string, tokens []string) ([]string, []string) {
+func scanList(ppfmt pp.PP, key string, input string, tokens []string) ([]string, []string) {
 	var list []string
 	readyForNext := true
 	for len(tokens) > 0 {
@@ -18,11 +18,11 @@ func scanList(ppfmt pp.PP, input string, tokens []string) ([]string, []string) {
 		case ")":
 			return list, tokens
 		case "(", "&&", "||", "!":
-			ppfmt.Errorf(pp.EmojiUserError, `Failed to parse %q: unexpected token %q`, input, tokens[0])
+			ppfmt.Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, tokens[0])
 			return nil, nil
 		default:
 			if !readyForNext {
-				ppfmt.Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, tokens[0])
+				ppfmt.Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, tokens[0])
 			}
 			list = append(list, tokens[0])
 			readyForNext = false
@@ -33,8 +33,8 @@ func scanList(ppfmt pp.PP, input string, tokens []string) ([]string, []string) {
 	return list, tokens
 }
 
-func scanASCIIDomainList(ppfmt pp.PP, input string, tokens []string) ([]string, []string) {
-	list, tokens := scanList(ppfmt, input, tokens)
+func scanASCIIDomainList(ppfmt pp.PP, key string, input string, tokens []string) ([]string, []string) {
+	list, tokens := scanList(ppfmt, key, input, tokens)
 	domains := make([]string, 0, len(list))
 	for _, raw := range list {
 		domains = append(domains, domain.StringToASCII(raw))
@@ -42,42 +42,43 @@ func scanASCIIDomainList(ppfmt pp.PP, input string, tokens []string) ([]string,
 	return domains, tokens
 }
 
-func scanDomainList(ppfmt pp.PP, input string, tokens []string) ([]domain.Domain, []string) {
-	list, tokens := scanList(ppfmt, input, tokens)
+func scanDomainList(ppfmt pp.PP, key string, input string, tokens []string) ([]domain.Domain, []string) {
+	list, tokens := scanList(ppfmt, key, input, tokens)
 	domains := make([]domain.Domain, 0, len(list))
 	for _, raw := range list {
 		domain, err := domain.New(raw)
 		if err != nil {
-			ppfmt.Warningf(pp.EmojiUserError,
-				"Domain %q was added but it is ill-formed: %v",
-				domain.Describe(), err)
+			ppfmt.Errorf(pp.EmojiUserError,
+				"%s (%q) contains an ill-formed domain %q: %v",
+				key, input, domain.Describe(), err)
+			return nil, nil
 		}
 		domains = append(domains, domain)
 	}
 	return domains, tokens
 }
 
-func scanConstants(_ppfmt pp.PP, _input string, tokens []string, wanted []string) (string, []string) {
+func scanConstants(_ppfmt pp.PP, _key string, _input string, tokens []string, expected []string) (string, []string) {
 	if len(tokens) == 0 {
 		return "", nil
 	}
-	for _, wanted := range wanted {
-		if wanted == tokens[0] {
+	for _, expected := range expected {
+		if expected == tokens[0] {
 			return tokens[0], tokens[1:]
 		}
 	}
 	return "", nil
 }
 
-func scanMustConstant(ppfmt pp.PP, input string, tokens []string, wanted string) []string {
+func scanMustConstant(ppfmt pp.PP, key string, input string, tokens []string, expected string) []string {
 	if len(tokens) == 0 {
-		ppfmt.Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; reached end of string`, input, wanted)
+		ppfmt.Errorf(pp.EmojiUserError, `%s (%q) is missing %q at the end`, key, input, expected)
 		return nil
 	}
-	if wanted == tokens[0] {
+	if expected == tokens[0] {
 		return tokens[1:]
 	}
-	ppfmt.Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; got %q`, input, wanted, tokens[0])
+	ppfmt.Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q when %q is expected`, key, input, tokens[0], expected)
 	return nil
 }
 
@@ -92,31 +93,31 @@ func hasStrictSuffix(s, suffix string) bool {
 // <factor> --> true | false | <fun> | ! <factor> | ( <expression> )
 //
 //nolint:funlen
-func scanFactor(ppfmt pp.PP, input string, tokens []string) (predicate, []string) {
+func scanFactor(ppfmt pp.PP, key string, input string, tokens []string) (predicate, []string) {
 	// fmt.Printf("scanFactor(tokens = %#v)\n", tokens)
 
-	if _, newTokens := scanConstants(ppfmt, input, tokens,
+	if _, newTokens := scanConstants(ppfmt, key, input, tokens,
 		[]string{"1", "t", "T", "TRUE", "true", "True"}); newTokens != nil {
 		return func(_ domain.Domain) bool { return true }, newTokens
 	}
 
-	if _, newTokens := scanConstants(ppfmt, input, tokens,
+	if _, newTokens := scanConstants(ppfmt, key, input, tokens,
 		[]string{"0", "f", "F", "FALSE", "false", "False"}); newTokens != nil {
 		return func(_ domain.Domain) bool { return false }, newTokens
 	}
 
 	{
 		//nolint:nestif
-		if funName, newTokens := scanConstants(ppfmt, input, tokens, []string{"is", "sub"}); newTokens != nil {
-			newTokens = scanMustConstant(ppfmt, input, newTokens, "(")
+		if funName, newTokens := scanConstants(ppfmt, key, input, tokens, []string{"is", "sub"}); newTokens != nil {
+			newTokens = scanMustConstant(ppfmt, key, input, newTokens, "(")
 			if newTokens == nil {
 				return nil, nil
 			}
-			ASCIIDomains, newTokens := scanASCIIDomainList(ppfmt, input, newTokens)
+			ASCIIDomains, newTokens := scanASCIIDomainList(ppfmt, key, input, newTokens)
 			if newTokens == nil {
 				return nil, nil
 			}
-			newTokens = scanMustConstant(ppfmt, input, newTokens, ")")
+			newTokens = scanMustConstant(ppfmt, key, input, newTokens, ")")
 			if newTokens == nil {
 				return nil, nil
 			}
@@ -145,9 +146,9 @@ func scanFactor(ppfmt pp.PP, input string, tokens []string) (predicate, []string
 	}
 
 	{
-		_, newTokens := scanConstants(ppfmt, input, tokens, []string{"!"})
+		_, newTokens := scanConstants(ppfmt, key, input, tokens, []string{"!"})
 		if newTokens != nil {
-			if pred, newTokens := scanFactor(ppfmt, input, newTokens); newTokens != nil {
+			if pred, newTokens := scanFactor(ppfmt, key, input, newTokens); newTokens != nil {
 				return func(d domain.Domain) bool { return !(pred(d)) }, newTokens
 			}
 			return nil, nil
@@ -155,13 +156,13 @@ func scanFactor(ppfmt pp.PP, input string, tokens []string) (predicate, []string
 	}
 
 	{
-		_, newTokens := scanConstants(ppfmt, input, tokens, []string{"("})
+		_, newTokens := scanConstants(ppfmt, key, input, tokens, []string{"("})
 		if newTokens != nil {
-			pred, newTokens := scanExpression(ppfmt, input, newTokens)
+			pred, newTokens := scanExpression(ppfmt, key, input, newTokens)
 			if newTokens == nil {
 				return nil, nil
 			}
-			newTokens = scanMustConstant(ppfmt, input, newTokens, ")")
+			newTokens = scanMustConstant(ppfmt, key, input, newTokens, ")")
 			if newTokens == nil {
 				return nil, nil
 			}
@@ -170,9 +171,9 @@ func scanFactor(ppfmt pp.PP, input string, tokens []string) (predicate, []string
 	}
 
 	if len(tokens) == 0 {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; reached end of string", input)
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression", key, input)
 	} else {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; got %q", input, tokens[0])
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression: got unexpected token %q", key, input, tokens[0])
 	}
 	return nil, nil
 }
@@ -180,20 +181,20 @@ func scanFactor(ppfmt pp.PP, input string, tokens []string) (predicate, []string
 // scanTerm scans a term with this grammar:
 //
 //	<term> --> <factor> "&&" <term> | <factor>
-func scanTerm(ppfmt pp.PP, input string, tokens []string) (predicate, []string) {
+func scanTerm(ppfmt pp.PP, key string, input string, tokens []string) (predicate, []string) {
 	// fmt.Printf("scanTerm(tokens = %#v)\n", tokens)
 
-	pred1, tokens := scanFactor(ppfmt, input, tokens)
+	pred1, tokens := scanFactor(ppfmt, key, input, tokens)
 	if tokens == nil {
 		return nil, nil
 	}
 
-	_, newTokens := scanConstants(ppfmt, input, tokens, []string{"&&"})
+	_, newTokens := scanConstants(ppfmt, key, input, tokens, []string{"&&"})
 	if newTokens == nil {
 		return pred1, tokens
 	}
 
-	pred2, newTokens := scanTerm(ppfmt, input, newTokens)
+	pred2, newTokens := scanTerm(ppfmt, key, input, newTokens)
 	if newTokens != nil {
 		return func(d domain.Domain) bool { return pred1(d) && pred2(d) }, newTokens
 	}
@@ -204,18 +205,18 @@ func scanTerm(ppfmt pp.PP, input string, tokens []string) (predicate, []string)
 // scanExpression scans an expression with this grammar:
 //
 //	<expression> --> <term> "||" <expression> | <term>
-func scanExpression(ppfmt pp.PP, input string, tokens []string) (predicate, []string) {
-	pred1, tokens := scanTerm(ppfmt, input, tokens)
+func scanExpression(ppfmt pp.PP, key string, input string, tokens []string) (predicate, []string) {
+	pred1, tokens := scanTerm(ppfmt, key, input, tokens)
 	if tokens == nil {
 		return nil, nil
 	}
 
-	_, newTokens := scanConstants(ppfmt, input, tokens, []string{"||"})
+	_, newTokens := scanConstants(ppfmt, key, input, tokens, []string{"||"})
 	if newTokens == nil {
 		return pred1, tokens
 	}
 
-	pred2, newTokens := scanExpression(ppfmt, input, newTokens)
+	pred2, newTokens := scanExpression(ppfmt, key, input, newTokens)
 	if newTokens != nil {
 		return func(d domain.Domain) bool { return pred1(d) || pred2(d) }, newTokens
 	}
@@ -224,17 +225,17 @@ func scanExpression(ppfmt pp.PP, input string, tokens []string) (predicate, []st
 }
 
 // ParseList parses a list of comma-separated domains. Internationalized domain names are fully supported.
-func ParseList(ppfmt pp.PP, input string) ([]domain.Domain, bool) {
-	tokens, ok := tokenize(ppfmt, input)
+func ParseList(ppfmt pp.PP, key string, input string) ([]domain.Domain, bool) {
+	tokens, ok := tokenize(ppfmt, key, input)
 	if !ok {
 		return nil, false
 	}
 
-	list, tokens := scanDomainList(ppfmt, input, tokens)
+	list, tokens := scanDomainList(ppfmt, key, input, tokens)
 	if tokens == nil {
 		return nil, false
 	} else if len(tokens) > 0 {
-		ppfmt.Errorf(pp.EmojiUserError, `Failed to parse %q: unexpected token %q`, input, tokens[0])
+		ppfmt.Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, tokens[0])
 		return nil, false
 	}
 
@@ -255,17 +256,17 @@ func ParseList(ppfmt pp.PP, input string) ([]domain.Domain, bool) {
 //   - exp1 && exp2, where exp1 and exp2 are boolean expressions, representing logical conjunction of exp1 and exp2.
 //
 // One can use parentheses to group expressions, such as !(is(hello.org) && (is(hello.io) || is(hello.me))).
-func ParseExpression(ppfmt pp.PP, input string) (predicate, bool) {
-	tokens, ok := tokenize(ppfmt, input)
+func ParseExpression(ppfmt pp.PP, key string, input string) (predicate, bool) {
+	tokens, ok := tokenize(ppfmt, key, input)
 	if !ok {
 		return nil, false
 	}
 
-	pred, tokens := scanExpression(ppfmt, input, tokens)
+	pred, tokens := scanExpression(ppfmt, key, input, tokens)
 	if tokens == nil {
 		return nil, false
 	} else if len(tokens) > 0 {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to parse %q: unexpected token %q", input, tokens[0])
+		ppfmt.Errorf(pp.EmojiUserError, "%s (%q) has unexpected token %q", key, input, tokens[0])
 		return nil, false
 	}
 
diff --git a/internal/droproot/checker.go b/internal/droproot/checker.go
new file mode 100644
index 0000000..f2092ef
--- /dev/null
+++ b/internal/droproot/checker.go
@@ -0,0 +1,46 @@
+package droproot
+
+import (
+	"strconv"
+	"strings"
+	"syscall"
+
+	"golang.org/x/exp/slices"
+
+	"github.com/favonia/cloudflare-ddns/internal/pp"
+)
+
+func checkUserID(ppfmt pp.PP, uid int) {
+	euid := syscall.Geteuid()
+
+	// Check if uid is the effective user ID.
+	if euid != uid {
+		ppfmt.Noticef(pp.EmojiUserWarning, "Failed to reset user ID to %d; current one: %d", uid, euid)
+	}
+}
+
+func checkGroupIDs(ppfmt pp.PP, gid int) bool {
+	egid := syscall.Getegid()
+	groups, err := syscall.Getgroups()
+	if err != nil {
+		ppfmt.Errorf(pp.EmojiImpossible, "Failed to get supplementary group IDs: %v", err)
+		return false
+	}
+
+	// Check if gid is the only effective group ID.
+	ok := egid == gid && !slices.ContainsFunc(groups, func(g int) bool { return g != gid })
+	if !ok {
+		descriptions := make([]string, 1, len(groups)+1)
+		descriptions[0] = strconv.Itoa(egid)
+		for _, g := range groups {
+			if g != egid {
+				descriptions = append(descriptions, strconv.Itoa(g))
+			}
+		}
+		ppfmt.Warningf(pp.EmojiUserWarning,
+			"Failed to reset group IDs to only %d; current ones: %s",
+			gid, strings.Join(descriptions, ", "))
+	}
+
+	return ok
+}
diff --git a/internal/droproot/config.go b/internal/droproot/config.go
new file mode 100644
index 0000000..91b67f6
--- /dev/null
+++ b/internal/droproot/config.go
@@ -0,0 +1,45 @@
+package droproot
+
+import (
+	"syscall"
+
+	"github.com/favonia/cloudflare-ddns/internal/config"
+	"github.com/favonia/cloudflare-ddns/internal/pp"
+)
+
+// readPUID reads PUID.
+func readPUID(ppfmt pp.PP) (int, bool) {
+	// Calculate the default user ID if PUID is not set
+	uid := syscall.Geteuid() // effective user ID
+	if uid == 0 {
+		uid = syscall.Getuid() // real user ID
+		if uid == 0 {
+			uid = 1000 // default, if everything is 0
+		}
+	}
+
+	// The target user ID, after taking PUID into consideration
+	if !config.ReadLinuxID(ppfmt, "PUID", &uid) {
+		return 0, false
+	}
+
+	return uid, true
+}
+
+// readPGID returns PGID.
+func readPGID(ppfmt pp.PP) (int, bool) {
+	// Calculate the default value of PGID
+	gid := syscall.Getegid() // effective group ID
+	if gid == 0 {
+		gid = syscall.Getgid() // real group ID
+		if gid == 0 {
+			gid = 1000 // default, if everything is 0 (root)
+		}
+	}
+
+	if !config.ReadLinuxID(ppfmt, "PGID", &gid) {
+		return 0, false
+	}
+
+	return gid, true
+}
diff --git a/internal/droproot/drop.go b/internal/droproot/drop.go
index 58738c1..c2914b0 100644
--- a/internal/droproot/drop.go
+++ b/internal/droproot/drop.go
@@ -2,13 +2,10 @@
 package droproot
 
 import (
-	"strconv"
-	"strings"
 	"syscall"
 
 	"kernel.org/pub/linux/libs/security/libcap/cap"
 
-	"github.com/favonia/cloudflare-ddns/internal/config"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
@@ -28,145 +25,73 @@ func tryRaiseCap(val cap.Value) {
 		return
 	}
 
-	if err := c.SetProc(); err != nil {
-		return
-	}
+	_ = c.SetProc()
 }
 
-// dropSuperuserGroup tries to set the group ID to something non-zero.
+// setGroups tries to set the group IDs.
 //
 // We do not call cap.SetGroups because of the following two reasons:
 //  1. cap.SetGroups will fail when the capability SETGID cannot be obtained,
 //     even if Setgid would have worked.
 //  2. We use Setresgid instead of Setgid to set all group IDs at once.
-func dropSuperuserGroup(ppfmt pp.PP) {
-	// Calculate the default group ID if PGID is not set
-	defaultGID := syscall.Getegid() // effective group ID
-	if defaultGID == 0 {
-		defaultGID = syscall.Getgid() // real group ID
-		if defaultGID == 0 {
-			defaultGID = 1000 // default, if everything is 0 (root)
-		}
-	}
-
-	// The target group ID, after taking PGID into consideration
-	gid := defaultGID
-	if !config.ReadNonnegInt(ppfmt, "PGID", &gid) {
-		gid = defaultGID
-	} else if gid == 0 {
-		ppfmt.Errorf(pp.EmojiUserError, "PGID cannot be 0. Using PGID=%d instead", defaultGID)
-		gid = defaultGID
-	}
-
+func setGroups(ppfmt pp.PP, gid int) bool {
 	// Try to raise cap.SETGID so that we can change our group ID
 	tryRaiseCap(cap.SETGID)
 
-	// First, erase all supplementary groups
-	if err := syscall.Setgroups([]int{}); err != nil {
-		ppfmt.Infof(pp.EmojiBullet, "Failed to erase supplementary GIDs (which might be fine): %v", err)
-	}
+	// Attempt to erase all supplementary groups and set the group ID
+	_ = syscall.Setgroups([]int{})
+	_ = syscall.Setresgid(gid, gid, gid)
 
-	// Now, set the group ID
-	if err := syscall.Setresgid(gid, gid, gid); err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to set GID to %d: %v", gid, err)
-	}
+	// Check whether the setting works
+	checkGroupIDs(ppfmt, gid)
+
+	return true
 }
 
-// dropSuperuser sets the user ID to something non-zero.
+// setUser sets the user ID to something non-zero.
 //
 // We do not call cap.SetUID because of the following two reasons:
 //  1. cap.SetUID will fail when the capability SETUID cannot be obtained,
 //     even if Setuid would have worked.
 //  2. We use Setresuid instead of Setuid to set all user IDs at once.
-func dropSuperuser(ppfmt pp.PP) {
-	// Calculate the default user ID if PUID is not set
-	defaultUID := syscall.Geteuid() // effective user ID
-	if defaultUID == 0 {
-		defaultUID = syscall.Getuid() // real user ID
-		if defaultUID == 0 {
-			defaultUID = 1000 // default, if everything is 0
-		}
-	}
-
-	// The target user ID, after taking PUID into consideration
-	uid := defaultUID
-	if !config.ReadNonnegInt(ppfmt, "PUID", &uid) {
-		uid = defaultUID
-	} else if uid == 0 {
-		ppfmt.Errorf(pp.EmojiUserError, "PUID cannot be 0. Using PUID=%d instead", defaultUID)
-		uid = defaultUID
-	}
-
+func setUser(ppfmt pp.PP, uid int) bool {
 	// Try to raise cap.SETUID so that we can change our user ID
 	tryRaiseCap(cap.SETUID)
 
 	// Now, set the user ID
-	if err := syscall.Setresuid(uid, uid, uid); err != nil {
-		ppfmt.Errorf(pp.EmojiUserError, "Failed to set UID to %d: %v", uid, err)
-	}
+	_ = syscall.Setresuid(uid, uid, uid)
+	checkUserID(ppfmt, uid)
+
+	return true
 }
 
 // dropCapabilities drop all capabilities as the last step.
-func dropCapabilities(ppfmt pp.PP) {
+func dropCapabilities(ppfmt pp.PP) bool {
 	if err := cap.NewSet().SetProc(); err != nil {
 		ppfmt.Errorf(pp.EmojiImpossible, "Failed to drop all capabilities: %v", err)
 	}
+	return true
 }
 
-// DropPriviledges drops all privileges as much as possible.
-func DropPriviledges(ppfmt pp.PP) {
+// DropPrivileges drops all privileges as much as possible.
+func DropPrivileges(ppfmt pp.PP) bool {
 	if ppfmt.IsEnabledFor(pp.Info) {
-		ppfmt.Infof(pp.EmojiPriviledges, "Dropping privileges . . .")
+		ppfmt.Infof(pp.EmojiPrivileges, "Dropping privileges . . .")
 		ppfmt = ppfmt.IncIndent()
 	}
 
-	// handle the group ID first, because the user ID could have given us the power
-	dropSuperuserGroup(ppfmt)
-
-	// handle the user ID
-	dropSuperuser(ppfmt)
-
-	// try to all remaining capabilities
-	dropCapabilities(ppfmt)
-}
-
-// printCapabilities prints out all remaining capabilities.
-func printCapabilities(ppfmt pp.PP) {
-	now := cap.GetProc()
-	diff, err := now.Cf(cap.NewSet())
-	if err != nil {
-		ppfmt.Errorf(pp.EmojiImpossible, "Failed to compare capabilities: %v", err)
-	} else if diff != 0 {
-		ppfmt.Errorf(pp.EmojiError, "The program still retains some additional capabilities: %v", now)
-	}
-}
-
-// describeGroups turns a list of integers {1, 2, 3} into a string "1 2 3".
-func describeGroups(gids []int) string {
-	if len(gids) == 0 {
-		return "(none)"
-	}
-
-	descriptions := make([]string, 0, len(gids))
-	for _, gid := range gids {
-		descriptions = append(descriptions, strconv.Itoa(gid))
+	uid, ok := readPUID(ppfmt)
+	if !ok {
+		return false
 	}
-	return strings.Join(descriptions, " ")
-}
-
-// PrintPriviledges prints out all remaining privileges.
-func PrintPriviledges(ppfmt pp.PP) {
-	ppfmt.Noticef(pp.EmojiPriviledges, "Remaining privileges:")
-	inner := ppfmt.IncIndent()
-
-	inner.Noticef(pp.EmojiBullet, "Effective UID:      %d", syscall.Geteuid())
-	inner.Noticef(pp.EmojiBullet, "Effective GID:      %d", syscall.Getegid())
 
-	if groups, err := syscall.Getgroups(); err != nil {
-		inner.Errorf(pp.EmojiImpossible, "Supplementary GIDs: (failed to get them)")
-	} else {
-		inner.Noticef(pp.EmojiBullet, "Supplementary GIDs: %s", describeGroups(groups))
+	gid, ok := readPGID(ppfmt)
+	if !ok {
+		return false
 	}
 
-	printCapabilities(inner)
+	// 1. Change the group ID first, because the user ID could have given us the power
+	// 2. Change the user ID
+	// 3. Drop all capabilities
+	return setGroups(ppfmt, gid) && setUser(ppfmt, uid) && dropCapabilities(ppfmt)
 }
diff --git a/internal/pp/emoji.go b/internal/pp/emoji.go
index 312bb3f..2627b3f 100644
--- a/internal/pp/emoji.go
+++ b/internal/pp/emoji.go
@@ -10,7 +10,7 @@ const (
 	EmojiEnvVars      Emoji = "📖" // reading configuration
 	EmojiConfig       Emoji = "🔧" // showing configuration
 	EmojiInternet     Emoji = "🌐" // network address detection
-	EmojiPriviledges  Emoji = "🥷" // /privileges
+	EmojiPrivileges   Emoji = "🥷" // /privileges
 	EmojiMute         Emoji = "🔇" // quiet mode
 	EmojiExperimental Emoji = "🧪" // experimental features
 


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 03b3319..5342313 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -222,7 +222,7 @@ func TestReadProviderMap(t *testing.T) {
 			},
 			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: not a valid provider", "flare")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a valid provider", "IP4_PROVIDER", "flare")
 			},
 		},
 	} {
@@ -245,7 +245,7 @@ func TestReadProviderMap(t *testing.T) {
 	}
 }
 
-//nolint:paralleltest // environment vars are global
+//nolint:paralleltest,funlen // environment vars are global
 func TestReadDomainMap(t *testing.T) {
 	for name, tc := range map[string]struct {
 		domains       string
@@ -282,6 +282,14 @@ func TestReadDomainMap(t *testing.T) {
 			true,
 			nil,
 		},
+		"ill-formed": {
+			" ", "   ", "*.*", nil, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError,
+					"%s (%q) contains an ill-formed domain %q: %v",
+					"IP6_DOMAINS", "*.*", "*.*", gomock.Any())
+			},
+		},
 	} {
 		tc := tc
 		t.Run(name, func(t *testing.T) {
@@ -530,6 +538,7 @@ func TestReadEnvEmpty(t *testing.T) {
 func TestNormalizeConfig(t *testing.T) {
 	t.Parallel()
 
+	keyProxied := "PROXIED"
 	var empty config.Config
 
 	for name, tc := range map[string]struct {
@@ -719,7 +728,7 @@ func TestNormalizeConfig(t *testing.T) {
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().IncIndent().Return(m),
-					m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; got %q", `range`, `range`),
+					m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression: got unexpected token %q", keyProxied, `range`, `range`), //nolint:lll
 				)
 			},
 		},
@@ -740,7 +749,7 @@ func TestNormalizeConfig(t *testing.T) {
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().IncIndent().Return(m),
-					m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; got %q", "999", "999"),
+					m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression: got unexpected token %q", keyProxied, `999`, `999`), //nolint:lll
 				)
 			},
 		},
@@ -761,7 +770,7 @@ func TestNormalizeConfig(t *testing.T) {
 					m.EXPECT().IsEnabledFor(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().IncIndent().Return(m),
-					m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; reached end of string`, `is(12345`, ")"),
+					m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is missing %q at the end`, keyProxied, `is(12345`, ")"),
 				)
 			},
 		},
diff --git a/internal/config/env_test.go b/internal/config/env_test.go
index 4447736..cf8d35f 100644
--- a/internal/config/env_test.go
+++ b/internal/config/env_test.go
@@ -135,7 +135,7 @@ func TestReadEmoji(t *testing.T) {
 		"illform": {
 			true, "weird", false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "weird", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, "weird", gomock.Any())
 			},
 		},
 	} {
@@ -182,7 +182,7 @@ func TestReadQuiet(t *testing.T) {
 		"illform": {
 			true, "weird", false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "weird", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, "weird", gomock.Any())
 			},
 		},
 	} {
@@ -203,6 +203,71 @@ func TestReadQuiet(t *testing.T) {
 	}
 }
 
+//nolint:paralleltest,funlen // environment vars are global
+func TestReadLinuxID(t *testing.T) {
+	key := keyPrefix + "ID"
+	for name, tc := range map[string]struct {
+		set           bool
+		val           string
+		oldField      int
+		newField      int
+		ok            bool
+		prepareMockPP func(*mocks.MockPP)
+	}{
+		"nil": {
+			false, "", 100, 100, true,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%d", key, 100)
+			},
+		},
+		"empty": {
+			true, "", 100, 100, true,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%d", key, 100)
+			},
+		},
+		"zero": {
+			true, "0   ", 100, 100, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) cannot be zero (the superuser)", key, 0)
+			},
+		},
+		"-1": {
+			true, "   -1", 100, 100, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) is negative", key, -1)
+			},
+		},
+		"1": {true, "   1   ", 100, 1, true, nil},
+		"1.0": {
+			true, "   1.0   ", 100, 100, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, "1.0", gomock.Any())
+			},
+		},
+		"words": {
+			true, "   word   ", 100, 100, false,
+			func(m *mocks.MockPP) {
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, "word", gomock.Any())
+			},
+		},
+	} {
+		tc := tc
+		t.Run(name, func(t *testing.T) {
+			set(t, key, tc.set, tc.val)
+			field := tc.oldField
+			mockCtrl := gomock.NewController(t)
+			mockPP := mocks.NewMockPP(mockCtrl)
+			if tc.prepareMockPP != nil {
+				tc.prepareMockPP(mockPP)
+			}
+			ok := config.ReadLinuxID(mockPP, key, &field)
+			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.newField, field)
+		})
+	}
+}
+
 //nolint:funlen,paralleltest // environment vars are global
 func TestReadBool(t *testing.T) {
 	key := keyPrefix + "BOOL"
@@ -245,13 +310,13 @@ func TestReadBool(t *testing.T) {
 		"illform1": {
 			true, "weird\t  ", false, false, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "weird", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, "weird", gomock.Any())
 			},
 		},
 		"illform2": {
 			true, " weird", true, true, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "weird", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean: %v", key, "weird", gomock.Any())
 			},
 		},
 	} {
@@ -298,20 +363,20 @@ func TestReadNonnegInt(t *testing.T) {
 		"-1": {
 			true, "   -1", 100, 100, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %d is negative", "-1", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) is negative", key, -1)
 			},
 		},
 		"1": {true, "   1   ", 100, 1, true, nil},
 		"1.0": {
 			true, "   1.0   ", 100, 100, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "1.0", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, "1.0", gomock.Any())
 			},
 		},
 		"words": {
 			true, "   word   ", 100, 100, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "word", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, "word", gomock.Any())
 			},
 		},
 	} {
@@ -351,32 +416,32 @@ func TestReadTTL(t *testing.T) {
 		"0": {
 			true, "0   ", api.TTLAuto, api.TTLAuto, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "TTL (%d) should be 1 (auto) or between 30 and 86400", 0)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) should be 1 (auto) or between 30 and 86400", key, 0)
 			},
 		},
 		"-1": {
 			true, "   -1", api.TTLAuto, api.TTLAuto, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "TTL (%d) should be 1 (auto) or between 30 and 86400", -1)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) should be 1 (auto) or between 30 and 86400", key, -1)
 			},
 		},
 		"1": {true, "   1   ", api.TTLAuto, api.TTLAuto, true, nil},
 		"20": {
 			true, "   20   ", api.TTLAuto, api.TTLAuto, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "TTL (%d) should be 1 (auto) or between 30 and 86400", 20)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) should be 1 (auto) or between 30 and 86400", key, 20)
 			},
 		},
 		"9999999": {
 			true, "   9999999   ", api.TTLAuto, api.TTLAuto, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "TTL (%d) should be 1 (auto) or between 30 and 86400", 9999999)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%d) should be 1 (auto) or between 30 and 86400", key, 9999999)
 			},
 		},
 		"words": {
 			true, "   word   ", api.TTLAuto, api.TTLAuto, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "TTL (%q) is not a number: %v", "word", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a number: %v", key, "word", gomock.Any())
 			},
 		},
 	} {
@@ -418,21 +483,21 @@ func TestReadDomains(t *testing.T) {
 		"test1":     {true, "書.org ,  Bücher.org  ", ds{f("random.org")}, ds{f("xn--rov.org"), f("xn--bcher-kva.org")}, true, nil},                      //nolint:lll
 		"test2":     {true, "  \txn--rov.org    ,   xn--Bcher-kva.org  ", ds{f("random.org")}, ds{f("xn--rov.org"), f("xn--bcher-kva.org")}, true, nil}, //nolint:lll
 		"illformed1": {
-			true, "xn--:D.org",
+			true, "xn--:D.org,a.org",
 			ds{f("random.org")},
-			ds{f("xn--:d.org")},
-			true,
+			ds{f("random.org")},
+			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiUserError, "Domain %q was added but it is ill-formed: %v", "xn--:d.org", gomock.Any()) //nolint:lll
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) contains an ill-formed domain %q: %v", key, "xn--:D.org,a.org", "xn--:d.org", gomock.Any()) //nolint:lll
 			},
 		},
 		"illformed2": {
-			true, "*.xn--:D.org",
+			true, "*.xn--:D.org,a.org",
 			ds{f("random.org")},
-			ds{w("xn--:d.org")},
-			true,
+			ds{f("random.org")},
+			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiUserError, "Domain %q was added but it is ill-formed: %v", "*.xn--:d.org", gomock.Any()) //nolint:lll
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) contains an ill-formed domain %q: %v", key, "*.xn--:D.org,a.org", "*.xn--:d.org", gomock.Any()) //nolint:lll
 			},
 		},
 		"illformed3": {
@@ -441,7 +506,7 @@ func TestReadDomains(t *testing.T) {
 			ds{},
 			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: unexpected token %q", "hi.org,(", "(")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) has unexpected token %q", key, "hi.org,(", "(")
 			},
 		},
 		"illformed4": {
@@ -450,7 +515,7 @@ func TestReadDomains(t *testing.T) {
 			ds{},
 			false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: unexpected token %q", ")", ")")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) has unexpected token %q", key, ")", ")")
 			},
 		},
 	} {
@@ -511,7 +576,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s and provider "cloudflare" were deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`, //nolint:lll
+					`%s=cloudflare is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
 					keyDeprecated, key, key,
 				)
 			},
@@ -521,7 +586,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s was deprecated; use %s=%s`,
+					`%s is deprecated; use %s=%s`,
 					keyDeprecated,
 					key,
 					"cloudflare.trace",
@@ -533,7 +598,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s was deprecated; use %s=%s`,
+					`%s is deprecated; use %s=%s`,
 					keyDeprecated,
 					key,
 					"cloudflare.doh",
@@ -545,7 +610,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s was deprecated; use %s=none`,
+					`%s is deprecated; use %s=none`,
 					keyDeprecated,
 					key,
 				)
@@ -556,7 +621,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s was deprecated; use %s=%s`,
+					`%s is deprecated; use %s=%s`,
 					keyDeprecated,
 					key,
 					"local",
@@ -568,7 +633,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Parameter %s and provider "ipify" were deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+					`%s=ipify is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
 					keyDeprecated,
 					key,
 					key,
@@ -578,7 +643,7 @@ func TestReadProvider(t *testing.T) {
 		"deprecated/others": {
 			false, "", true, "   something-else ", ipify, ipify, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: not a valid provider", "something-else")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a valid provider", keyDeprecated, "something-else")
 			},
 		},
 		"conflicts": {
@@ -602,7 +667,7 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Errorf(
 					pp.EmojiUserError,
-					`Parameter %s does not accept "cloudflare"; use %s=cloudflare.trace or %s=cloudflare.doh`,
+					`%s=cloudflare is invalid; use %s=cloudflare.trace or %s=cloudflare.doh`,
 					key, key, key,
 				)
 			},
@@ -616,7 +681,8 @@ func TestReadProvider(t *testing.T) {
 			func(m *mocks.MockPP) {
 				m.EXPECT().Warningf(
 					pp.EmojiUserWarning,
-					`Provider "ipify" was deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+					`%s=ipify is deprecated; use %s=cloudflare.trace or %s=cloudflare.doh`,
+					key,
 					key,
 					key,
 				)
@@ -625,7 +691,7 @@ func TestReadProvider(t *testing.T) {
 		"others": {
 			true, "   something-else ", false, "", ipify, ipify, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: not a valid provider", "something-else")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a valid provider", key, "something-else")
 			},
 		},
 	} {
@@ -674,13 +740,13 @@ func TestReadNonnegDuration(t *testing.T) {
 		"1": {
 			true, "  1  ", 123, 123, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "1", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a time duration: %v", key, "1", gomock.Any())
 			},
 		},
 		"-1s": {
 			true, "  -1s  ", 456, 456, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v is negative", "-1s", -time.Second)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%v) is negative", key, -time.Second)
 			},
 		},
 		"0h": {true, "  0h  ", 123456, 0, true, nil},
@@ -739,7 +805,7 @@ func TestReadCron(t *testing.T) {
 		"illformed": {
 			true, " @ddddd  ", cron.MustNew("*/4 * * * *"), cron.MustNew("*/4 * * * *"), false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "@ddddd", gomock.Any())
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a cron expression: %v", key, "@ddddd", gomock.Any())
 			},
 		},
 	} {
diff --git a/internal/domainexp/parser_test.go b/internal/domainexp/parser_test.go
index 3e880b8..b5be183 100644
--- a/internal/domainexp/parser_test.go
+++ b/internal/domainexp/parser_test.go
@@ -14,6 +14,7 @@ import (
 
 func TestParseList(t *testing.T) {
 	t.Parallel()
+	key := "key"
 	type f = domain.FQDN
 	type w = domain.Wildcard
 	type ds = []domain.Domain
@@ -32,16 +33,16 @@ func TestParseList(t *testing.T) {
 			ds{f("a"), f("b"), f("c"), f("d")},
 			func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, "b"),
-					m.EXPECT().Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, "c"),
-					m.EXPECT().Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, "d"),
+					m.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, " a b c d ", "b"),
+					m.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, " a b c d ", "c"),
+					m.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, " a b c d ", "d"),
 				)
 			},
 		},
 		"illformed/1": {
 			"&", false, nil,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "&", domainexp.ErrSingleAnd)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "&", domainexp.ErrSingleAnd)
 			},
 		},
 	} {
@@ -55,7 +56,7 @@ func TestParseList(t *testing.T) {
 				tc.prepareMockPP(mockPP)
 			}
 
-			list, ok := domainexp.ParseList(mockPP, tc.input)
+			list, ok := domainexp.ParseList(mockPP, key, tc.input)
 			require.Equal(t, tc.ok, ok)
 			require.Equal(t, tc.expected, list)
 		})
@@ -65,6 +66,7 @@ func TestParseList(t *testing.T) {
 //nolint:funlen
 func TestParseExpression(t *testing.T) {
 	t.Parallel()
+	key := "key"
 	type f = domain.FQDN
 	type w = domain.Wildcard
 	for name, tc := range map[string]struct {
@@ -77,7 +79,7 @@ func TestParseExpression(t *testing.T) {
 		"empty": {
 			"", false, nil, true,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted a boolean expression; reached end of string`, "")
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression`, key, "")
 			},
 		},
 		"const/1": {"true", true, nil, true, nil},
@@ -86,38 +88,38 @@ func TestParseExpression(t *testing.T) {
 		"&&/2": {
 			"t &&", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted a boolean expression; reached end of string`, "t &&") //nolint:lll
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression`, key, "t &&")
 			},
 		},
 		"&&/&/1": {
 			"true & true", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "true & true", domainexp.ErrSingleAnd)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "true & true", domainexp.ErrSingleAnd)
 			},
 		},
 		"&&/&/2": {
 			"true &", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "true &", domainexp.ErrSingleAnd)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "true &", domainexp.ErrSingleAnd)
 			},
 		},
 		"||/1": {"F || 1", true, nil, true, nil},
 		"||/2": {
 			"F ||", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted a boolean expression; reached end of string`, "F ||") //nolint:lll
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression`, key, "F ||")
 			},
 		},
 		"||/|/1": {
 			"false | false", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "false | false", domainexp.ErrSingleOr)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "false | false", domainexp.ErrSingleOr)
 			},
 		},
 		"||/|/2": {
 			"false |", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: %v", "false |", domainexp.ErrSingleOr)
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is ill-formed: %v", key, "false |", domainexp.ErrSingleOr)
 			},
 		},
 		"is/1":          {"is(example.com)", true, f("example.com"), true, nil},
@@ -132,19 +134,19 @@ func TestParseExpression(t *testing.T) {
 		"is/error/1": {
 			"is)", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; got %q`, "is)", "(", ")")
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q when %q is expected`, key, "is)", ")", "(")
 			},
 		},
 		"is/error/2": {
 			"is(&&", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: unexpected token %q`, "is(&&", "&&")
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, "is(&&", "&&")
 			},
 		},
 		"is/error/3": {
 			"is", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; reached end of string`, "is", "(")
+				m.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is missing %q at the end`, key, "is", "(")
 			},
 		},
 		"sub/1":     {"sub(example.com)", true, f("example.com"), false, nil},
@@ -159,26 +161,26 @@ func TestParseExpression(t *testing.T) {
 		"not/3": {
 			"!(", false, nil, true,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; reached end of string", "!(")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression", key, "!(")
 			},
 		},
 		"nested/1": {"((true)||(false))&&((false)||(true))", true, nil, true, nil},
 		"nested/2": {
 			"((", false, nil, true,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: wanted a boolean expression; reached end of string", "((")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is not a boolean expression", key, "((")
 			},
 		},
 		"nested/3": {
 			"(true", false, nil, true,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: wanted %q; reached end of string", "(true", ")")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) is missing %q at the end", key, "(true", ")")
 			},
 		},
 		"error/extra": {
 			"0 1", false, nil, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Errorf(pp.EmojiUserError, "Failed to parse %q: unexpected token %q", "0 1", "1")
+				m.EXPECT().Errorf(pp.EmojiUserError, "%s (%q) has unexpected token %q", key, "0 1", "1")
 			},
 		},
 	} {
@@ -192,7 +194,7 @@ func TestParseExpression(t *testing.T) {
 				tc.prepareMockPP(mockPP)
 			}
 
-			pred, ok := domainexp.ParseExpression(mockPP, tc.input)
+			pred, ok := domainexp.ParseExpression(mockPP, "key", tc.input)
 			require.Equal(t, tc.ok, ok)
 			if ok {
 				require.Equal(t, tc.expected, pred(tc.domain))
diff --git a/test/fuzzer/fuzzer_test.go b/test/fuzzer/fuzzer_test.go
index e0e486d..8d1d9eb 100644
--- a/test/fuzzer/fuzzer_test.go
+++ b/test/fuzzer/fuzzer_test.go
@@ -11,18 +11,20 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
+const key string = "KEY"
+
 // FuzzParseList fuzz test [domainexp.ParseList].
 func FuzzParseList(f *testing.F) {
 	f.Fuzz(func(t *testing.T, input string) {
 		mockCtrl := gomock.NewController(t)
 		mockPP := mocks.NewMockPP(mockCtrl)
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: %v`, gomock.Any(), domainexp.ErrSingleAnd).AnyTimes()
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: %v`, gomock.Any(), domainexp.ErrSingleOr).AnyTimes()
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: unexpected token %q`, gomock.Any(), gomock.Any()).AnyTimes()        //nolint:lll
-		mockPP.EXPECT().Warningf(pp.EmojiUserError, `Domain %q was added but it is ill-formed: %v`, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
-		mockPP.EXPECT().Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, gomock.Any()).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleAnd).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleOr).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, gomock.Any()).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) contains an ill-formed domain %q: %v`, key, input, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
+		mockPP.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, gomock.Any()).AnyTimes()                 //nolint:lll
 
-		_, _ = domainexp.ParseList(mockPP, input)
+		_, _ = domainexp.ParseList(mockPP, key, input)
 	})
 }
 
@@ -31,16 +33,16 @@ func FuzzParseExpression(f *testing.F) {
 	f.Fuzz(func(t *testing.T, input string) {
 		mockCtrl := gomock.NewController(t)
 		mockPP := mocks.NewMockPP(mockCtrl)
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: %v`, gomock.Any(), domainexp.ErrSingleAnd).AnyTimes()
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: %v`, gomock.Any(), domainexp.ErrSingleOr).AnyTimes()
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: unexpected token %q`, gomock.Any(), gomock.Any()).AnyTimes()                  //nolint:lll
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; got %q`, gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()      //nolint:lll
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted %q; reached end of string`, gomock.Any(), gomock.Any()).AnyTimes()     //nolint:lll
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted a boolean expression; got %q`, gomock.Any(), gomock.Any()).AnyTimes()  //nolint:lll
-		mockPP.EXPECT().Errorf(pp.EmojiUserError, `Failed to parse %q: wanted a boolean expression; reached end of string`, gomock.Any()).AnyTimes() //nolint:lll
-		mockPP.EXPECT().Warningf(pp.EmojiUserError, "Domain %q was added but it is ill-formed: %v", gomock.Any(), gomock.Any()).AnyTimes()           //nolint:lll
-		mockPP.EXPECT().Warningf(pp.EmojiUserError, `Please insert a comma "," before %q`, gomock.Any()).AnyTimes()
-
-		_, _ = domainexp.ParseExpression(mockPP, input)
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleAnd).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is ill-formed: %v`, key, input, domainexp.ErrSingleOr).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q`, key, input, gomock.Any()).AnyTimes()                                   //nolint:lll
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) has unexpected token %q when %q is expected`, key, input, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is missing %q at the end`, key, input, gomock.Any()).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression: got unexpected token %q`, key, input, gomock.Any()).AnyTimes() //nolint:lll
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) is not a boolean expression`, key, input).AnyTimes()
+		mockPP.EXPECT().Errorf(pp.EmojiUserError, `%s (%q) contains an ill-formed domain %q: %v`, key, input, gomock.Any(), gomock.Any()).AnyTimes() //nolint:lll
+		mockPP.EXPECT().Warningf(pp.EmojiUserError, `%s (%q) is missing a comma "," before %q`, key, input, gomock.Any()).AnyTimes()                 //nolint:lll
+
+		_, _ = domainexp.ParseExpression(mockPP, key, input)
 	})
 }