外壳命名空间

外壳命名空间

有没有办法将sourceshell 脚本放入命名空间,最好是 bash shell 脚本,但我会研究其他 shell,如果它们有此功能而 bash 没有。

我的意思是,例如,“为所有定义的符号添加前缀,以便它们不会与已定义的符号(变量名称、函数名称、别名)发生冲突”或任何其他防止名称冲突的设施。

如果有一个解决方案可以让我同时命名空间sourceNodeJS样式),那将是最好的。

示例代码:

$ echo 'hi(){ echo Hello, world; }' > english.sh
$ echo 'hi(){ echo Ahoj, světe; }' > czech.sh
$ . english.sh
$ hi
 #=> Hello, world
$ . czech.sh #bash doesn't even warn me that `hi` is being overwritten here
$ hi
 #=> Ahoj, světe
#Can't use the English hi now
#And sourcing the appropriate file before each invocation wouldn't be very efficient 

答案1

从安装了...man ksh的系统上ksh93

  • 命名空间
    • 作为修改变量或创建新变量的命令列表的一部分执行的命令和函数namespace,创建一个新变量,其名称是由前面的标识符给定的名称空间的名称.。当引用名称为 name 的变量时,首先使用.identifier.name
    • 类似地,命名空间列表中的命令定义的函数是使用以 开头的命名空间名称创建的.
    • 当命名空间命令列表包含namespace命令时,创建的变量和函数的名称由变量或函数名称组成,每个名称前面都有标识符列表.。在名称空间之外,可以通过在名称空间之前添加名称空间名称来引用在名称空间内创建的变量或函数。
    • 默认情况下,以 开头的变量.sh位于sh名称空间。

并且,为了演示,这里是将概念应用于默认情况下为ksh93shell 中分配的每个常规 shell 变量提供的命名空间。在下面的例子中我将定义一个discipline函数将充当shell 变量.get的指定方法$PS1。每个 shell 变量基本上都有自己的命名空间,至少具有默认的getsetappendunset方法。定义以下函数后,每次在 shell 中引用该变量时,都会在屏幕顶部绘制$PS1的输出...date

function PS1.get {
    printf "\0337\33[H\33[K%s\0338" "${ date; }"
}

()(另请注意上述命令替换中缺少子shell)

从技术上来说,命名空间学科不是确切地一样的东西(因为学科可以定义为全局或局部应用于特定的名称空间,但它们都是 shell 数据类型概念化的重要组成部分,而 shell 数据类型是ksh93.

为了解决您的特定示例:

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!

...或者...

for ns in czech english
do  ".$ns.hi"
done

Ahoj, světe!
Hello, World!

答案2

我编写了一个 POSIX shell 函数,可用于在ksh93dashmksh或中的任何一个中对 shell 内置函数或函数进行本地命名空间bash (特别命名是因为我亲自确认它适用于所有这些)。在我测试过的 shell 中,它只是未能满足我的期望yash,而且我从未期望它能在 中工作zsh。我没有测试posh。我之前就放弃了任何希望,posh并且有一段时间没有安装它了。也许它适用于posh...?

我说它是 POSIX,因为根据我对规范的阅读,它利用了基本实用程序的指定行为,但是,无可否认,规范在这方面是模糊的,并且至少一个人显然不同意我的观点。一般来说,我对此有异议,我最终发现错误是我自己的,而且可能这次我在规范上也错了,但当我进一步询问他时,他没有回复。

不过,正如我所说,这在上述 shell 中确实有效,并且基本上按以下方式工作:

some_fn(){ x=3; echo "$x"; }
x=
x=local command eval some_fn
echo "${x:-empty}"

3
empty

command命令被指定为基本可用的实用程序和预先内置的命令之一$PATH。它的指定功能之一是在调用特殊的内置实用程序时将其包装在其自己的环境中,等等......

{       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

...根据规范,上述两个命令行分配的行为都是正确的。两种错误条件的行为也是正确的,并且实际上几乎完全与规范重复。指定函数或特殊内置函数命令行前缀的赋值会影响当前 shell 环境。同样,当指向其中任何一个时,重定向错误将被指定为致命错误。command指定以抑制在这些情况下对特殊内置函数的特殊处理,并且重定向情况实际上通过规范中的示例进行了演示。

command另一方面,常规内置函数(例如)被指定在子shell环境- 这并不一定意味着另一个过程,只是它应该与一个根本上没有区别。调用常规内置函数的结果应该始终类似于从类似功能的$PATH'd 命令中获得的结果。所以...

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

但该command命令无法调用 shell 函数,因此不能像对常规内置函数那样用于使它们的特殊处理变得毫无意义。这也是指定的。事实上,规范说,它的主要用途command是您可以在为另一个命令命名的包装 shell 函数中使用它来调用另一个命令,而无需自递归,因为它不会调用该函数。像这样:

cd(){ command cd -- "$1"; }

如果您不在command那里使用该cd函数几乎肯定会出现自递归的段错误。

但是作为一个可以调用特殊内置函数的常规内置函数command可以在子shell环境。因此,虽然当前定义的 shell 状态可能会坚持当前的 shell - 当然是read并且$var1确实$var2如此 - 至少命令行定义的结果可能不应该......

简单命令

如果没有命令名结果,或者命令名是特殊的内置函数或函数,则变量赋值将影响当前的执行环境。否则,变量赋值应导出到命令的执行环境,并且不会影响当前的执行环境。

现在是否能够command同时成为常规内置直接调用特殊的内置函数只是关于命令行定义的某种意外漏洞我不知道,但我确实知道至少已经提到的四个 shell 尊重command命名空间。

虽然command不能直接调用 shell 函数,但它如演示的那样调用eval,因此可以间接执行此操作。所以我根据这个概念构建了一个命名空间包装器。它需要一个参数列表,例如:

ns any=assignments or otherwise=valid names which are not a command then all of its args

...除了command上面的单词只有在可以找到空的情况下才被识别为一个$PATH。除了在命令行上命名的本地作用域 shell 变量之外,它还对具有单个小写字母名称的所有变量以及其他标准名称的列表进行本地作用域,例如$PS3$PS4$OPTARG$OPTIND$IFS$PATH$PWD$OLDPWD其他一些变量。

是的,通过在本地确定$PWD$OLDPWD变量的范围,然后显式地cd确定范围$OLDPWD$PWD它也可以相当可靠地确定当前工作目录的范围。尽管它确实非常努力,但并不能保证这一点。它保留一个描述符,7<.并且当它的包装目标返回时它会保留一个描述符cd -P /dev/fd/7/。如果当前工作目录unlink()在此期间已被更改,它至少仍应设法更改回原来的工作目录,但在这种情况下会发出一个丑陋的错误。而且因为它维护描述符,所以我认为理智的内核也不应该允许卸载其根设备(???)

它还在本地范围内设置 shell 选项,并在其包装的实用程序返回时将这些选项恢复到找到它们时的状态。它的$OPTS特殊对待是,它在自己的范围内维护一个副本,并最初为其分配值$-。在处理完命令行上的所有分配之后,它将set -$OPTS在调用其换行目标之前执行此操作。通过这种方式,如果您-$OPTS在命令行上定义,则可以定义包装目标的 shell 选项。当目标返回时,它将set +$- -$OPTS带着自己的副本$OPTS (不受命令行定义的影响)并将一切恢复到原来的状态。

当然,没有什么可以阻止调用者returrn通过包装目标或其参数以某种方式显式退出函数。这样做将阻止它尝试进行的任何状态恢复/清理。

要做到这一切,需要深入三eval心。首先,它将自身包装在本地作用域中,然后,从内部读取参数,验证它们是否有效,如果找到不合法的 shell 名称,则错误退出。如果所有参数均有效且最终有一个参数command -v "$1"返回 true(回想一下:$PATH此时为空)它将eval命令行定义并将所有剩余参数传递给包装目标(尽管它忽略了 - 的特殊情况ns,因为这不是很有用,而且 3 evals 的深度已经足够深了)

它基本上是这样工作的:

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<.

还有一些其他重定向,以及一些奇怪的测试,这些测试与某些 shell 放入c然后$-拒绝接受它作为选项的方式有关set (???),但它都是辅助的,主要用于避免发出不需要的输出以及在边缘情况下类似的输出。这就是它的工作原理。它可以做这些事情,因为它在调用嵌套的实用程序之前设置自己的本地范围。

它很长,因为我在这里要非常小心——三个evals很难。但有了它你可以做到:

ns X=local . /dev/fd/0 <<""; echo "$X" "$Y"
X=still_local
Y=global
echo "$X" "$Y"

still_local global
 global

更进一步,持久地命名包装实用程序的本地范围应该不会很困难。即使按照书面形式,它也已经$LOCALS为包装实用程序定义了一个变量,该变量仅由在包装实用程序环境中定义的所有名称的空格分隔列表组成。

喜欢:

ns var1=something var2= eval ' printf "%-10s%-10s%-10s%s\n" $LOCALS '

...这是完全安全的 -$IFS已被清理为其默认值,并且只有有效的 shell 名称才能进入,$LOCALS除非您自己在命令行上设置它。即使拆分变量中可能存在全局字符,您OPTS=f也可以在命令行上设置包装实用程序以禁止它们扩展。任何状况之下:

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      

这是函数。所有命令都带有前缀 w/\以避免alias扩展:

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
}

相关内容