5

In dash (and in bash zsh and some other shells) the command local works to make a variable scope limited to that function (and descendants in some cases). That enable the possibility of making a variable limited to that function internals only (and descendant function calls in some cases).

For example:

testlocal(){ 
                local IFS
                IFS=123
                echo "internal  IFS = $IFS"
                testdescend
}

testdescend(){
                echo "descended IFS = $IFS"
}

IFS=abc
testlocal
echo "external  IFS = $IFS"

Produce this output on execution in dash (and bash and zsh):

$ dash ./script
internal  IFS = 123
descended IFS = 123
external  IFS = abc

Which means that the IFS remains local to the function (and descendants).

However, ksh is different, doesn't accept local:

$ ksh ./script
./mt2[3]: local: not found [No such file or directory]

Changing the word 'local' to 'typeset' makes the script (almost) work in ksh but dash doesn't recognize typeset.

And, to make the scope dynamic (descend to called functions) in ksh, the word function must be used to define functions:

function testlocal { 
                     typeset IFS
                     IFS=123
                     echo "internal IFS = $IFS"
                     testdescend
                   }

Which further complicates portability to dash.

The question is: How to make the original script work also in ksh ? Is it possible to avoid a test for which shell is running the script?

  • 2
    Add #!/usr/bin/env bash to 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 8 at 18:59
  • 1
    You can often use a subshell instead. – Mikel Sep 8 at 19:22
  • 4
    Just save the value of IFS and restore it afterwards. Stick to POSIX features if you want portability. – Wildcard Sep 8 at 19:37
  • @Mikel Subshells may be an order of magnitude slower (Try it online!) – Isaac Sep 9 at 23:12
  • @Wildcard It becomes quite ugly to cover an originally unset IFS. – Isaac Sep 9 at 23:15
2

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:

  • ksh93 only does static scoping False. – Isaac Sep 11 at 12:38
  • 2
    @Isaac, it seems like you're wanting to play on words, so I'm not going to argue with you. I'll leave the answer there in case it can be useful to others. – Stéphane Chazelas Sep 11 at 14:15
  • @Isaac This is a confusion in terminology. When Stéphane says "ksh93 only does static scoping", he means ksh93 has both "no scope" and "static scope". "No scope" means global scope only, i.e. there's only one (global) scope for variables. Static (or private) scope means that a variable is only visible within the declaring function, not in any children functions, including itself if called recursively. Dynamic scope means visible and changeable by children functions. In both static and dynamic scope, the previous scope is restored when the function returns to its caller. – jrw32982 Sep 13 at 17:09
  • Bash and ksh88 have global and dynamic scopes. Ksh93 has global and static scopes. Confusingly, the builtin "local" indicates dynamic scope, not private (static) scope. See also unix.stackexchange.com/questions/375156. – jrw32982 Sep 13 at 17:09
0

Partial solution

No local used (none at all).

I have not found a fully working solution, but a partial one that works on dash, bash, and zsh (at least). Just call the function with an environment variable set:

IFS=123 testlocal

You can test at (Try it online!).

The shell ksh (yes ksh93 in the server used) is a bit more complex as it has dual personality. But both personalities are presented at the link above.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.