[PATCH] var: Call setlocale when LC variables are set

Herbert Xu <[email protected]> Sat, 13 Jun 2026 20:25:27 +0800
Newsgroups org.kernel.vger.dash
Message-ID <[email protected]>
Vladimir Kudrya <[email protected]> wrote:
> Hi!
> It seems that this commit c5bf9702 changed behavior of ${var#?} under 
> LC_ALL=C, which should consider characters to be one byte long and cut 
> accordingly.
> 
> This code:
> 
>        var=联;
>        LC_ALL=C
>        while [ -n "$var" ]; do
>                cut_var=${var#?}
>                byte=${var%"$cut_var"}
>                printf '>%02x<\n' "'$byte"
>                var=$cut_var
>        done
>        unset LC_ALL
> 
> should output:
> 
> >e8<
> >81<
> >94<
> 
> It works reliably in sh 0.5.12, also ash, bash, zsh (both in POSIX and 
> native modes), but is broken in dash 0.5.13.
> 
> More context can be found at:
> https://github.com/Vladimir-csp/app2unit/issues/17

Yes this is a consequence of dash only calling setlocale on startup.

This patch should fix the problem:

---8<---
When any shell-relevant locale environment variable changes, call
putenv followed by setlocale so that the change is effective for
the current shell.

Reported-by: Vladimir Kudrya <[email protected]>
Fixes: cb669294463e ("shell: Call setlocale")
Signed-off-by: Herbert Xu <[email protected]>

diff --git a/src/main.c b/src/main.c
index 5d25b8d..2ab506c 100644
--- a/src/main.c
+++ b/src/main.c
@@ -32,7 +32,6 @@
  * SUCH DAMAGE.
  */
 
-#include <locale.h>
 #include <stdio.h>
 #include <signal.h>
 #include <sys/stat.h>
@@ -104,8 +103,6 @@ main(int argc, char **argv)
 	monitor(4, etext, profile_buf, sizeof profile_buf, 50);
 #endif
 
-	setlocale(LC_ALL, "");
-
 	state = 0;
 	if (unlikely(setjmp(main_handler.loc))) {
 		int e;
diff --git a/src/var.c b/src/var.c
index 90fc896..e76abe3 100644
--- a/src/var.c
+++ b/src/var.c
@@ -32,6 +32,7 @@
  * SUCH DAMAGE.
  */
 
+#include <locale.h>
 #include <unistd.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -81,6 +82,12 @@ MKINIT char defoptindvar[] = "OPTIND=1";
 int lineno;
 char linenovar[sizeof("LINENO=")+sizeof(int)*CHAR_BIT/3+1] = "LINENO=";
 
+static void changelocale(const char *val)
+{
+	putenv((char *)val);
+	setlocale(LC_ALL, nullstr);
+}
+
 /* Some macros in var.h depend on the order, add new variables to the end. */
 struct var varinit[] = {
 #if ATTY
@@ -101,6 +108,11 @@ struct var varinit[] = {
 	{ 0,	VSTRFIXED|VTEXTFIXED|VUNSET,	"TERM\0",	0 },
 	{ 0,	VSTRFIXED|VTEXTFIXED|VUNSET,	"HISTSIZE\0",	sethistsize },
 #endif
+	{ 0,	VSTRFIXED|VTEXTFIXED|VFULL|VUNSET, "LC_ALL\0",	changelocale },
+	{ 0,	VSTRFIXED|VTEXTFIXED|VFULL|VUNSET, "LC_COLLATE\0", changelocale },
+	{ 0,	VSTRFIXED|VTEXTFIXED|VFULL|VUNSET, "LC_CTYPE\0", changelocale },
+	{ 0,	VSTRFIXED|VTEXTFIXED|VFULL|VUNSET, "LC_NUMERIC\0", changelocale },
+	{ 0,	VSTRFIXED|VTEXTFIXED|VFULL|VUNSET, "LANG\0",	changelocale },
 };
 
 STATIC struct var *vartab[VTABSIZE];
@@ -160,6 +172,19 @@ static char *varnull(const char *s)
 	return strchrnul(s, '=') + 1;
 }
 
+static void varfunc(struct var *vp)
+{
+	const char *s;
+
+	if (!vp->func)
+		return;
+
+	s = vp->text;
+	if (!(vp->flags & VFULL))
+		s = varnull(s);
+	(*vp->func)(s);
+}
+
 /*
  * This routine initializes the builtin variables.  It is called when the
  * shell is initialized.
@@ -299,8 +324,8 @@ out_free:
 	vp->text = s;
 	vp->flags = flags;
 
-	if (vp->func && (flags & VNOFUNC) == 0)
-		(*vp->func)(varnull(s));
+	if (!(flags & VNOFUNC))
+		varfunc(vp);
 
 out:
 	return vp;
@@ -536,8 +561,8 @@ poplocalvars(void)
 				ckfree(vp->text);
 			vp->flags = lvp->flags;
 			vp->text = lvp->text;
-			if (vp->func && !(vp->flags & VNOFUNC))
-				(*vp->func)(varnull(vp->text));
+			if (!(vp->flags & VNOFUNC))
+				varfunc(vp);
 		}
 		ckfree(lvp);
 	}
diff --git a/src/var.h b/src/var.h
index f6fb320..3d837a7 100644
--- a/src/var.h
+++ b/src/var.h
@@ -48,7 +48,7 @@
 #define VSTACK		0x10	/* text is allocated on the stack */
 #define VUNSET		0x20	/* the variable is not set */
 #define VNOFUNC		0x40	/* don't call the callback function */
-/* #define VNOSET	0x80	   do not set variable - just readonly test */
+#define VFULL		0x80	/* pass value suitable for putenv */
 #define VNOSAVE		0x100	/* when text is on the heap before setvareq */
 
 
-- 
Email: Herbert Xu <[email protected]>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt