Shell namespaces
From man ksh
on a system with a ksh93
installed...
- Name Spaces
- Commands and functions that are executed as part of the list of a
namespace
command that modify variables or create new ones, create a new variable whose name is the name of the name space as given by identifier preceded by.
. When a variable whose name is name is referenced, it is first searched for using.identifier.name
.- Similarly, a function defined by a command in the namespace list is created using the name space name preceded by a
.
.- When the list of a namespace command contains a
namespace
command, the names of variables and functions that are created consist of the variable or function name preceded by the list of identifiers each preceded by.
. Outside of a name space, a variable or function created inside a name space can be referenced by preceding it with the name space name.- By default, variables staring with
.sh
are in thesh
name space.
And, to demonstrate, here is the concept applied to a namespace provided by default for every regular shell variable assigned in a ksh93
shell. In the following example I will define a discipline
function that will act as the assigned .get
method for the $PS1
shell variable. Every shell variable basically gets its own namespace with, at least, the default get
, set
, append
, and unset
methods. After defining the following function, any time the variable $PS1
is referenced in the shell, the output of date
will be drawn at the top of the screen...
function PS1.get {
printf "\0337\33[H\33[K%s\0338" "${ date; }"
}
(Also note the lack of the ()
subshell in the above command substitution)
Technically, namespaces and disciplines are not exactly the same thing (because disciplines can be defined to apply either globally or locally to a particular namespace), but they are both part and parcel to the conceptualization of shell data types which is fundamental to ksh93
.
To address your particular examples:
echo 'function hi { echo Ahoj, světe\!; }' > czech.ksh
echo 'function hi { echo Hello, World\!; }' >english.ksh
namespace english { . ./english.ksh; }
namespace czech { . ./czech.ksh; }
.english.hi; .czech.hi
Hello, World!
Ahoj, světe!
...or...
for ns in czech english
do ".$ns.hi"
done
Ahoj, světe!
Hello, World!
I have written a POSIX shell function which might be used to locally namespace a shell builtin or function in any of ksh93
, dash
, mksh
, or bash
(named specifically because I have personally confirmed it to work in all of these). Of the shells in which I tested it, it only failed to meet my expectations in yash
, and I never expected it to work at all in zsh
. I did not test posh
. I gave up any hope for posh
a while ago and haven't installed it in some time. Maybe it works in posh
...?
I say it is POSIX because, by my reading of the specification, it takes advantage of a specified behavior of a basic utility, but, admittedly, the specification is vague in this regard, and, at least one person apparently disagrees with me. Generally I have had a disagreement with this one I have eventually found the error to be my own, and possibly I am also wrong this time about the specification as well, but when I questioned him further he did not reply.
As I said, though, this definitely does work in the aforementioned shells, and it works, basically, in the following way:
some_fn(){ x=3; echo "$x"; }
x=
x=local command eval some_fn
echo "${x:-empty}"
3
empty
The command
command is specified as a basically available utility and one of the pre-$PATH
'd builtins. One of its specified functions is to wrap special builtin utilities in its own environment when calling them, and so...
{ sh -c ' x=5 set --; echo "$x"
x=6 command set --; echo "$x"
exec <""; echo uh_oh'
sh -c ' command exec <""; echo still here'
}
5
5
sh: 3: cannot open : No such file
sh: 1: cannot open : No such file
still here
...the behavior of both command-line assignments above is correct by spec. The behavior of both error conditions is also correct, and is in fact very nearly completely duplicated there from the specification. Assignments prefixed to the command-lines of functions or special builtins are specified to affect the current shell environment. Likewise, redirection errors are specified as fatal when pointed at either of those. command
is specified to suppress the special treatment of special builtins in those cases, and the redirection case is actually demonstrated by example in the specification.
Regular builtins, like command
, on the other hand, are specified to run in a subshell environment - which doesn't necessarily mean that of another process, just that it should be fundamentally indistinguishable from one. The results of calling a regular builtin ought always to resemble what might be obtained from a similarly capable $PATH
'd command. And so...
na=not_applicable_to_read
na= read var1 na na var2 <<"" ; echo "$var1" "$na" "$var2"
word1 other words word2
word1 not_applicable_to_read word2
But the command
command cannot call shell functions, and so cannot be used to render their special treatment moot as it can for regular builtins. That's also spec'd. In fact, the spec says that a primary utility of command
is that you can use it within a wrapper shell function named for another command to call that other command without self-recursion because it will not call the function. Like this:
cd(){ command cd -- "$1"; }
If you didn't use command
there the cd
function would almost definitely segfault for self-recursion.
But as a regular builtin which can call special builtins command
can do so in a subshell environment. And so, while current shell-state defined within might stick to the current shell - certainly read
's $var1
and $var2
did - at least the results of command-line defines probably should not...
Simple Commands
If no command name results, or if the command name is a special built-in or function, variable assignments shall affect the current execution environment. Otherwise, the variable assignments shall be exported for the execution environment of the command and shall not affect the current execution environment.
Now whether or not command
's ability to be both a regular builtin and to directly call special builtins is just some kind of unexpected loophole with regard to command-line defines I don't know, but I do know that at least the four shells already mentioned honor the command
namespace.
And though command
cannot directly call shell functions, it can call eval
as demonstrated, and so can do so indirectly. So I built a namespace wrapper on this concept. It takes a list of arguments like:
ns any=assignments or otherwise=valid names which are not a command then all of its args
...except that the command
word above is only recognized as one if it can be found with an empty $PATH
. Besides locally-scoping shell variables named on the command line, it also locally-scopes all variable with single lower-case alphabetic names and a list of other standard ones, such as $PS3
, $PS4
, $OPTARG
, $OPTIND
, $IFS
, $PATH
, $PWD
, $OLDPWD
and some others.
And yes, by locally scoping the $PWD
and $OLDPWD
variables and afterward explicitly cd
ing to $OLDPWD
and $PWD
it can fairly reliably scope the current working directory as well. This isn't guaranteed, though it does try pretty hard. It retains a descriptor for 7<.
and when its wrap target returns it does cd -P /dev/fd/7/
. If the current working directory has been unlink()
'd in the interim it should still at least manage to change back into it but will emit an ugly error in that case though. And because it maintains the descriptor, I don't think a sane kernel should allow its root device to be unmounted either (???).
It also locally scopes shell options, and restores these to the state in which it found them when its wrapped utility returns. It treats $OPTS
specially in that it maintains a copy in its own scope which it initially assigns the value of $-
. After also handling all assignments on the command-line it will do set -$OPTS
just before invoking its wrap target. In this way if you define -$OPTS
on the command-line you can define a your wrap target's shell options. When the target returns it will set +$- -$OPTS
with its own copy of $OPTS
(which is not affected by the command-line defines) and restore all to the original state.
Of course, there's nothing stopping the caller from somehow explicitly returrn
ing out of the function by way of the wrap target or its arguments. Doing so will prevent any state restoration/cleanup it would otherwise attempt.
To do all that it needs to go three eval
's deep. First it wraps itself in a local scope, then, from within, it reads in arguments, validates them for valid shell names, and quits with error if it finds one which isn't. If all arguments are valid and eventually one causes command -v "$1"
to return true (recall: $PATH
is empty at this point) it will eval
the command-line defines and pass off all remaining arguments to the wrap target (though it ignores the special case for ns
- because that wouldn't be very useful, and three eval
s deep is more than deep enough).
It basically works like this:
case $- in (*c*) ... # because set -c doesnt work
esac
_PATH=$PATH PATH= OPTS=$- some=vars \
command eval LOCALS=${list_of_LOCALS}'
for a do i=$((i+1)) # arg ref
if [ "$a" != ns ] && # ns ns would be silly
command -v "$a" &&
! alias "$a" # aliases are hard to run quoted
then eval " PATH=\$_PATH OTHERS=$DEFAULTS $v \
command eval '\''
shift $((i-1)) # leave only tgt in @
case $OPTS in (*different*)
set \"-\${OPTS}\" # init shell opts
esac
\"\$@\" # run simple command
set +$- -$OPTS "$?" # save return, restore opts
'\''"
cd -P /dev/fd/7/ # go whence we came
return "$(($??$?:$1))" # return >0 for cd else $1
else case $a in (*badname*) : get mad;;
# rest of arg sa${v}es
esac
fi; done
' 7<.
There are some other redirections and, and a few weird tests to do with the way some shells put c
in $-
and then refuse to accept it as an option to set
(???), but its all ancillary, and primarily used just to save from emitting unwanted output and similar in edge cases. And that's how it works. It can do those things because it sets up its own local scope before calling its wrapped utility in a nested such.
It's long, because I try to be very careful here - three evals
is hard. But with it you can do:
ns X=local . /dev/fd/0 <<""; echo "$X" "$Y"
X=still_local
Y=global
echo "$X" "$Y"
still_local global
global
Taking that a step further and persistently namespacing the local scope of the wrapped utility shouldn't be very difficult. And even as written it already defines a $LOCALS
variable to the wrapped utility which is comprised of only a space separated list of all names that it defined in the wrapped utility's environment.
Like:
ns var1=something var2= eval ' printf "%-10s%-10s%-10s%s\n" $LOCALS '
...which is perfectly safe - $IFS
has been sanitized to its default value and only valid shell names make it into $LOCALS
unless you set it yourself on the command line. And even if there might be glob characters in a split variable, you could set OPTS=f
on the command-line as well for the wrapped utility to prohibit their expansion. In any case:
LOCALS ARG0 ARGC HOME
IFS OLDPWD OPTARG OPTIND
OPTS PATH PS3 PS4
PWD a b c
d e f g
h i j k
l m n o
p q r s
t u v w
x y z _
bel bs cr esc
ht ff lf vt
lb dq ds rb
sq var1 var2
And here's the function. All of the commands are prefixed w/ \
to avoid alias
expansions:
ns(){ ${1+":"} return
case $- in
(c|"") ! set "OPTS=" "$@"
;; (*c*) ! set "OPTS=${-%c*}${-#*c}" "$@"
;; (*) set "OPTS=$-" "$@"
;; esac
OPTS=${1#*=} _PATH=$PATH PATH= LOCALS= lf='
' rb=\} sq=\' l= a= i=0 v= __=$_ IFS=" ""
" command eval LOCALS=\"LOCALS \
ARG0 ARGC HOME IFS OLDPWD OPTARG OPTIND OPTS \
PATH PS3 PS4 PWD a b c d e f g h i j k l m n \
o p q r s t u v w x y z _ bel bs cr esc ht ff \
lf vt lb dq ds rb sq'"
for a do i=$((i+1))
if \[ ns != "$a" ] &&
\command -v "$a" >&9 &&
! \alias "${a%%=*}" >&9 2>&9
then \eval 7>&- '\' \
'ARGC=$((-i+$#)) ARG0=$a HOME=~' \
'OLDPWD=$OLDPWD PATH=$_PATH IFS=$IFS' \
'OPTARG=$OPTARG PWD=$PWD OPTIND=1' \
'PS3=$PS3 _=$__ PS4=$PS4 LOCALS=$LOCALS' \
'a= b= c= d= e= f= g= i=0 j= k= l= m= n= o=' \
'p= q= r= s= t= u= v= w= x=0 y= z= ht=\ ' \
'cr=^M bs=^H ff=^L vt=^K esc=^[ bel=^G lf=$lf' \
'dq=\" sq=$sq ds=$ lb=\{ rb=\}' \''"$v' \
'\command eval 9>&2 2>&- '\' \
'\shift $((i-1));' \
'case \${OPTS##*[!A-Za-z]*} in' \
'(*[!c$OPTS]*) >&- 2>&9"'\' \
'\set -"${OPTS%c*}${OPTS#*c}"' \
';;esac; "$@" 2>&9 9>&-; PS4= ' \
'\set +"${-%c*}${-#*c}"'\'\" \
-'$OPTS \"\$?\"$sq";' \
' \cd -- "${OLDPWD:-$PWD}"
\cd -P ${ANDROID_SYSTEM+"/proc/self/fd/7"} /dev/fd/7/
\return "$(($??$?:$1))"
else case ${a%%=*} in
([0-9]*|""|*[!_[:alnum:]]*)
\printf "%s: \${$i}: Invalid name: %s\n" \
>&2 "$0: ns()" "'\''${a%%=*}'\''"
\return 2
;; ("$a") v="$v $a=\$$a"
;; (*) v="$v ${a%%=*}=\${$i#*=}"
;; esac
case " $LOCALS " in (*" ${a%%=*} "*)
;; (*) LOCALS=$LOCALS" ${a%%=*}"
;; esac
fi
done' 7<. 9<>/dev/null
}