Note ksh88 and all its clones do dynamic scoping and have been supporting local for it since at least 1990 for ksh88 and 1994 for pdksh (and 1989 for bash which (now) implements a lot of the ksh88 API).
The ksh you're referring to is ksh93, a newer implementation from scratch by David Korn with a slightly different and incompatible API.
It's a good idea to refer to that shell as ksh93 instead of just ksh as ksh alone has meant the ksh88 API for a few decades (the one all but the ksh93 implementations implement). ksh93 has not been in wide use until a few years after 2000 when its code was released as open source.
ksh93's typeset only does static scoping. POSIX had objected to specifying ksh88's typeset/local on the ground that it was dynamic scoping (though, even though most languages like C do static scoping, dynamic scoping comes more naturally in shells as it's what you get in effect with subshells or with the environment), which explains why ksh93 was rewritten to do static coping.
Later, most other shells implemented scoping a la ksh88, so ksh93 is now the odd one out. And now ironically, POSIX's only reasonable option would be to specify a dynamic scope. While David Korn initially refused to implement dynamic scoping in ksh93, he had said he could consider it with the local keyword/builtin on the POSIX mailing list, but he has retired before that fully happened.
The ksh93v- beta and final version from AT&T can be compiled with an experimental "bash" mode (actually enabled by default) that does dynamic scoping (in bother forms of functions, including with local and typeset) when ksh93 is invoked as bash. That bash mode will be disabled by default in ksh2020 though the local/declare aliases to typeset will be retained even when the bash mode is not compiled in (though still with static scoping).
Now if we leave that beta release aside and its bash mode, ksh93 only does static scoping and only in the functions declared with the ksh-style syntax (function name { code; }). You're getting confused because the functions declared with the Bourne-style syntax (f() command) don't do scoping at all. It's the same with sourced files or ksh functions invoked with . name [args]. In those, typeset doesn't declare a new variable in the function's scope (that function has no scope), it just updates the type of the variable in the current scope which is going to be either the global scope or the scope of a ksh-style function if that Bourne-style was (eventually) called from within a ksh-style function.
The code of Bourne-style functions is run as if embedded/copy-pasted/sourced wherever they're invoked.
var=global
function ksh_function {
typeset var=private
echo "ksh1: $var"
bourne_function
echo "ksh2: $var"
other_ksh_function other
echo "ksh3: $var"
. other_ksh_function other_invoked_with_dot
echo "ksh4: $var"
}
bourne_function() {
typeset var=set-from-bourne-function
}
function other_ksh_function {
echo "other: $var"
var=$1
}
ksh_function
echo "global: $var"
Gives:
ksh1: private
ksh2: set-from-bourne-function
other: global
ksh3: set-from-bourne-function
other: set-from-bourne-function
ksh4: other_invoked_with_dot
global: other
It's not possible in ksh93 to have dynamic scoping other than by using subshells or implementing a variable stack by yourself like in that locvar proof of concept or you export the variable for it to be passed to every command (including functions declared with the ksh-style functions, including external commands via the environment).
In your particular case where only the testlocal function (and not testdescend) need a local scope, you can use the shdef+kshdef approach as described there or do something like:
case $KSH_VERSION in
(*" 93"*)
fn_with_local_scope() {
alias local=typeset
eval "function $1 {
$(cat)
}"
}
;;
(*)
fn_with_local_scope() {
eval "$1() {
$(cat)
}"
}
;;
esac
And then declare your functions as:
fn_with_local_scope testlocal << '}'
local IFS
IFS=123
echo "internal IFS = $IFS"
testdescend
}
testdescend(){
echo "descended IFS = $IFS"
}
IFS=abc
testlocal
echo "external IFS = $IFS"
(and only use local in functions declared with fn_with_local_scope).
Which gives
internal IFS = 123
descended IFS = 123
external IFS = abc
in all shells (note that you need a recent version of yash (2.48 or above) for it to support local).
Or if you're OK for the local variables to also be exported (in ksh93 only):
case $KSH_VERSION in
(*" 93"*)
fn() {
alias local='typeset -x'
eval "function $1 {
$(cat)
}"
}
;;
(*)
fn() {
eval "$1() {
$(cat)
}"
}
;;
esac
fn testlocal << '}'
local IFS
IFS=123
echo "internal IFS = $IFS"
testdescend
}
fn testdescend << '}'
echo "descended IFS = $IFS"
}
IFS=abc
testlocal
echo "external IFS = $IFS"
Now, if you were to do something like that in a language with static scoping like C or ksh93, you'd do something like:
function testlocal {
typeset IFS
IFS=123
echo "internal IFS = $IFS"
testdescend "$IFS"
}
function testdescend {
typeset IFS="$1" # explicitly get the value $IFS from the caller
echo "descended IFS = $IFS"
}
Which seems to me like a better design, and that code would work OK as well in shells that do dynamic scoping (you'd still need to address the different function definition syntax).
Further reading:
#!/usr/bin/env bashto the top of the file and you're good. Seriously. Trying to write actually portable shell scripts is an exercise in pain and futility, IMO, and maintainers are going to absolutely hate the result. Or you could./do-the-thing-in-most-shells.sh || ./do-the-thing-in-ksh.ksh. – l0b0 Sep 08 '19 at 18:59typeset/localand ksh93 has static scoping only and withfunction f {...;}only and withtypesetonly.f() {...;}functions have no local scope at all there. – Stéphane Chazelas Sep 10 '19 at 05:37local? : Most of them except ksh93. (2) I am using ksh93. (3) Dynamic vs. Static in ksh93 ? Care to explain why Try it online! prints14 14 13 12Is it only Static ?. The ksh93 has both scopes available. (4) You propose a complex workaround to have the function only appear in ksh ? ... @StéphaneChazelas – Sep 10 '19 at 22:30local. @StéphaneChazelas – Sep 10 '19 at 23:22bash script– Jonas Berlin Sep 11 '19 at 07:57