6

I thought typeset was ksh's local, but this fails in ksh93 although it works in all my other typeset-supporting shells (bash, yash, zsh, pdksh)

#!/bin/ksh -ex

foo(){
    typeset a b
    a=0; b=1
    return
}
a=a; b=b
foo
#confirm that the globals didn't change
[ "$a" = a ] 
[ "$b" = b ]

What gives?

Petr Skocik
  • 28,816

1 Answers1

11

typeset is ksh93's private (using static scoping like perl's my, not local which does dynamic scoping) only for functions that are declared using the ksh function definition style:

function foo {
  typeset var=whatever
  ...
}

With the Bourne syntax (or with the . command (which btw, can also be used on ksh-style functions)), there's no scoping (except for $1, $2... $# of course). So one can use Bourne-style functions to get the value or change the value or type of a variable in the parent context (though typeset -n can also be used for that with the ksh-style.

In ksh88, typeset was doing dynamic scoping with both the ksh and Bourne function definition style. According to David Korn, POSIX did not specify ksh's variable scoping on the basis that it was dynamic (deemed inferior) which is why he changed it to static scoping for ksh93 (a complete rewrite).

But in the mean time, other shells have implemented variable scoping and they all did it using dynamic scoping to mimic ksh88's.

zsh now has a private keyword to have scoping similar to ksh93's in addition to local/typeset with dynamic scoping like in ksh88.

To see the difference between static and dynamic scoping, compare:

"$shell" -c 'function f { typeset a=1; g; echo "$a"; }
             function g { echo "$a"; a=2; }
             a=0; f'

Which with $shell == ksh93 outputs:

0
1

And with ksh88 or bash outputs:

1
2

zsh:

$ zsh -c 'zmodload zsh/param/private
          f() { private a=1; g; echo $a;}
          g() { echo $a; a=2; }
          a=0; f'
0
1

To be able to use local scope in code portable to bash, zsh, ksh88 ksh93, pdksh, yash or dash/FreeBSD sh, you could do:

[ -n "$BASH_VERSION" ] && shopt -s expand_aliases
alias shdef= kshdef='#'
if type typeset > /dev/null 2>&1; then
  alias mylocal=typeset
  if (a=1; f() { typeset a=2; }; f; [ "$a" = 2 ]); then
    alias shdef='#' kshdef='function'
  fi
else
  alias mylocal=local
fi

And then declare your functions as:

kshdef foo
shdef foo()
{
  mylocal var
  var=value
  ...
}

In any case, there are many differences between the behaviour of those local in the various shells. Beside the dynamic vs static consideration mentioned above, there's whether variables initially get an unset or empty value or inherit the value from the parent scope. And there's the interaction with readonly, unset, whether local/typeset is a keyword or builtin (affects split+glob handling)...

There are other implications of using the ksh-style function definition in ksh93, see the man page for details.

More reading

  • 1
    Thanks. Do you know if it's possible to get the same effect as local in ksh93 in without the function-based function definitions? The reason I'm asking is I have a bunch of mostly POSIX code except with local variables and so I thought I'd make it more portable by replacing the local declarations with eval $current_shells_local_keyword $the_variables. – Petr Skocik Jul 04 '17 at 09:42
  • @PSkocik, you may want to have a look at https://github.com/modernish/modernish – Stéphane Chazelas Jul 04 '17 at 09:43
  • 1
    @PSkocik, see edit for one approach. – Stéphane Chazelas Jul 04 '17 at 10:08
  • I like your approach but ksh support wasn't essential, and I ended up ignoring it and simply doing alias local=typeset for yash. With that, my code appears to be correct in dash, bash, yash, posh, and zsh, as long as I don't rely on a specific type of scoping, the local values being auto-initialized to empty (in some shells they aren't), and on local being a keyword ( local a=$b is nonportable but local a="$b" or local a; a=$b appears to work across all the shells). Thanks for the help. – Petr Skocik Jul 05 '17 at 15:36
  • Even worse, the version of ksh93 on RHEL6.10 (shown as sh (AT&T Research) 93u+ 2012-08-01) and the version on RHEL7.8 (same version string) have different typeset behaviors, similar to the ksh88/ksh93 changes. For example: echo $HOME; typeset -u HOME; echo $HOME. On RHEL6 this uppercases $HOME. On RHEL7 this uppercases and clears $HOME. – jrw32982 Sep 09 '21 at 15:14
  • @jrw32982, looks more like a bug. Apparently fixed at https://github.com/ksh93/ksh. I'm surprised RHEL 6 (from 2010) would have u+ though. Are you sure that ksh came from redhat? – Stéphane Chazelas Sep 09 '21 at 16:36
  • By "bug", do you mean the rhel6 behavior or the rhel7 behavior? I can't be 100% sure where it came from. rpm -q shows ksh-20120801-38.el6_10.x86_64 on my rhel6.10 server. (On my rhel7.8 server it shows ksh-20120801-142.el7.x86_64) I found a copy of that rhel6.10 rpm on the internet and pulled it apart. The RELEASE file top line says 12-08-01...ksh93u+. However the ksh93 binary is a different size than on the server, so I don't know what to think. – jrw32982 Sep 09 '21 at 21:02
  • Running ksh93 from the downloaded package produces sh (AT&T Research) 93u+ 2012-08-01 and typeset -u does not clear an exported variable, such as HOME. – jrw32982 Sep 10 '21 at 14:20
  • @jrw32982, I suspect it's a regression introduced by a later fix (and later fixed upstreams, but not pulled by redhat yet). – Stéphane Chazelas Sep 10 '21 at 14:41
  • @jrw32982, might have something to do with CVE-2019-14868 which I reported a while back. – Stéphane Chazelas Sep 10 '21 at 14:50
  • In ksh93, is the expected "correct" behavior that typeset -u VAR should clear VAR (in this case an exported environment variable, not inside a function but in the mainline of a script) and uppercase further assignments to VAR, or should it uppercase the current value of VAR (prior to typeset) and all further assignments? I realize that the answer to this type of question is dependent on the particular shell/version being discussed. Is it clear what the answer is supposed to be for ksh93? – jrw32982 Sep 10 '21 at 18:40
  • @jrw32982, typeset -u var is meant to turn var to uppercase when called in the global scope, Clearing it (and unexporting it) instead would clearly be a bug to me. One that has already been fixed upstreams it seems as I said earlier. – Stéphane Chazelas Sep 10 '21 at 21:49
  • The regression would have happened much earlier than that CVE-2019-14868 though. I can see ksh93u already cleared the variable, while ksh93s+ was fine. – Stéphane Chazelas Sep 10 '21 at 21:58