Namespaces de shell

10

Existe uma maneira de sourceum script de shell em um espaço para nome, de preferência um script de shell bash, mas eu procuraria em outros shells se eles tivessem esse recurso e o bash não.

O que quero dizer com isso é, por exemplo, algo como "prefixar todos os símbolos definidos com algo para que eles não colidam com símbolos já definidos (nomes de variáveis, nomes de funções, aliases)" ou qualquer outro recurso que impeça colisões de nomes.

Se houver uma solução em que eu possa namespace no sourcemomento ( NodeJSestilo), isso seria o melhor.

Código de exemplo:

$ 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 
PSkocik
fonte
1
Obrigado pelo esclarecimento. Espero que a resposta seja negativa. O paradigma usual de programação de shell é que, quando você deseja isolar as alterações, você faz isso em um subshell, criando uma delas ( easiest thing ever ). Mas não é exatamente isso que você procura. Eu acho que você poderia fazer ( stuff in subshell; exec env ) | sed 's/^/namespace_/'e evalo resultado no shell pai, mas isso é meio desagradável.
Celada
3
Sim. Get ksh93. Os namespaces são fundamentais para ele - e todos os seus tipos de nomes (que também são tipificáveis) oferecem suporte ao namespacing. Também é muito mais rápido em praticamente todos os aspectos do que bash, a propósito.
mikeserv
@ mikeserv Obrigado, se você adicioná-lo como resposta com um exemplo de código que demonstra a funcionalidade, eu aceito.
PSKocik
@michas Eu precisaria também de nomes de funções de símbolos e apelidos. env | sed ...funcionaria para variáveis, eu poderia fazer setpara obter funções, mas a pesquisa e a substituição seriam um problema - as funções podem se chamar e você precisará substituir todas as invocações cruzadas por invocações cruzadas prefixadas, mas sem substituir o mesmas palavras em outras partes do código de definição de função, onde não é uma invocação. Para isso, você precisaria de um analisador de bash, não apenas de um regex, e ainda funcionaria apenas desde que as funções não se chamassem por eval.
PSKocik

Respostas:

11

A partir man kshde um sistema com um ksh93...

  • Espaços de nome
    • Comandos e funções que são executados como parte da lista de um namespacecomando que modifica variáveis ​​ou cria novas, cria uma nova variável cujo nome é o nome do espaço de nome conforme fornecido pelo identificador precedido por .. Quando uma variável cujo nome é nome é referenciada, ela é pesquisada primeiro usando .identifier.name.
    • Da mesma forma, uma função definida por um comando na lista de namespaces é criada usando o nome do namespace precedido por a ..
    • Quando a lista de um comando de espaço para nome contém um namespacecomando, os nomes de variáveis ​​e funções criadas consistem no nome da variável ou função precedida pela lista de identificadores, cada um precedido por .. Fora de um espaço para nome, uma variável ou função criada dentro de um espaço para nome pode ser referenciada precedendo-o com o nome do espaço para nome.
    • Por padrão, as variáveis ​​marcadas com .shestão no shespaço de nome.

E, para demonstrar, eis o conceito aplicado a um espaço de nome fornecido por padrão para todas as variáveis ​​regulares do shell atribuídas em um ksh93shell. No exemplo a seguir, definirei uma disciplinefunção que atuará como o .getmétodo atribuído à $PS1variável shell. Cada variável shell basicamente obtém seu próprio espaço de nomes com, pelo menos, o padrão get, set, append, e unsetmétodos. Após definir a função a seguir, sempre que a variável $PS1for referenciada no shell, a saída de dateserá desenhada na parte superior da tela ...

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

(Observe também a falta do ()subshell na substituição de comando acima)

Tecnicamente, os espaços para nome e as disciplinas não são exatamente a mesma coisa (porque as disciplinas podem ser definidas para aplicar global ou localmente a um espaço para nome específico ) , mas são parte integrante da conceitualização dos tipos de dados do shell, que são fundamentais para isso ksh93.

Para abordar seus exemplos específicos:

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!

...ou...

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

Ahoj, světe!
Hello, World!
mikeserv
fonte
@PSkocik - por que você não corrigiu minha coisa de ehoj ? Eu poderia jurar que foi o que disse antes ... desculpe por isso. Eu não aceitaria uma resposta escrita por alguém que nem se desse ao trabalho de soletrar corretamente as palavras que usei na pergunta ... Honestamente, porém, acho que me lembro de apenas copiar / colar jt ... hmm ...
Mikeerv
2

Eu escrevi uma função shell POSIX que pode ser usado para localmente namespace A builtin shell ou função em qualquer um ksh93, dash, mksh, ou bash (nomeado especificamente porque eu, pessoalmente, confirmou ao trabalho em todos estes) . Das conchas em que o testei, ele apenas falhou em atender às minhas expectativas yashe nunca esperei que funcionasse zsh. Eu não testei posh. Desisti de qualquer esperança há poshalgum tempo e não o instalo há algum tempo. Talvez funcione em posh...?

Eu digo que é POSIX porque, pela minha leitura da especificação, tira proveito de um comportamento especificado de um utilitário básico, mas, reconhecidamente, a especificação é vaga a esse respeito e, pelo menos uma pessoa aparentemente discorda de mim. Geralmente, eu tive uma discordância com essa, e finalmente encontrei o erro como meu, e possivelmente também estou errado desta vez com a especificação, mas quando o questionei mais, ele não respondeu.

Como eu disse, no entanto, isso definitivamente funciona nas conchas acima mencionadas, e funciona, basicamente, da seguinte maneira:

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

3
empty

O commandcomando é especificado como um utilitário basicamente disponível e um dos pré- $PATHinstalados. Uma de suas funções especificadas é agrupar utilitários embutidos especiais em seu próprio ambiente ao chamá-los, e assim ...

{       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

... o comportamento das duas atribuições de linha de comando acima está correto por especificação. O comportamento de ambas as condições de erro também está correto e, de fato, é quase completamente duplicado na especificação. As atribuições prefixadas às linhas de comando de funções ou recursos especiais especiais são especificadas para afetar o ambiente atual do shell. Da mesma forma, os erros de redirecionamento são especificados como fatais quando apontados para qualquer um deles. commandé especificado para suprimir o tratamento especial de recursos internos especiais nesses casos, e o caso de redirecionamento é realmente demonstrado por exemplo na especificação.

Construções regulares, como command, por outro lado, são especificadas para serem executadas em um ambiente de subcamadas - o que não significa necessariamente o de outro processo , apenas que ele deve ser fundamentalmente indistinguível de um. Os resultados de chamar um built-in regular sempre devem se parecer com o que pode ser obtido de um $PATHcomando com capacidade semelhante . E entao...

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

Mas o commandcomando não pode chamar funções de shell e, portanto, não pode ser usado para tornar seu tratamento especial discutível, como é possível para os componentes internos regulares. Isso também é especificado. De fato, a especificação diz que um utilitário primário commandé que você pode usá-lo em uma função shell do invólucro chamada para outro comando para chamar esse outro comando sem auto-recursão, porque não chamará a função. Como isso:

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

Se você não usasse commandlá, a cdfunção quase definitivamente segfault para auto-recursão.

Porém, como um embutido regular que pode chamar de embutidos especiais, commandpode fazê-lo em um ambiente de subcamação . E assim, embora o estado atual do shell definido dentro possa se ater ao shell atual - certamente reado fez $var1e o $var2fez - pelo menos os resultados da linha de comando definem provavelmente não deveriam ...

Comandos simples

Se nenhum nome de comando resultar, ou se o nome do comando for uma função ou built-in especial, as atribuições de variáveis ​​afetarão o ambiente de execução atual. Caso contrário, as atribuições de variáveis ​​serão exportadas para o ambiente de execução do comando e não afetarão o ambiente de execução atual.

Agora, se commanda capacidade de ser ou não um construtor regular e chamar diretamente de construtores especiais é apenas algum tipo de brecha inesperada no que diz respeito às definições da linha de comando, eu não sei, mas sei que pelo menos os quatro escudos já mencionado honra o commandespaço para nome.

E embora commandnão possa chamar diretamente funções de shell, pode chamar evalcomo demonstrado, e pode fazê-lo indiretamente. Então, eu criei um wrapper de espaço para nome nesse conceito. É preciso uma lista de argumentos como:

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

... exceto que a commandpalavra acima é reconhecida apenas como uma, se puder ser encontrada com um vazio $PATH. Além localmente escopo variáveis do shell nomeadas na linha de comando, ele também localmente escopos todos variável com minúsculas nomes alfabéticos individuais e uma lista de outras padrão, como $PS3, $PS4, $OPTARG, $OPTIND, $IFS, $PATH, $PWD, $OLDPWDe alguns outros.

E sim, por escopo localmente o $PWDe $OLDPWDvariáveis e depois explicitamente cding para $OLDPWDe $PWDele pode razoavelmente confiável âmbito do diretório de trabalho atual também. Isso não é garantido, embora tente bastante. Ele mantém um descritor para 7<.e quando seu destino de quebra retorna cd -P /dev/fd/7/. Se o diretório de trabalho atual estiver unlink()temporário, ele ainda deve pelo menos conseguir voltar para ele, mas emitirá um erro feio nesse caso. E como ele mantém o descritor, também não acho que um kernel sadio permita que seu dispositivo raiz seja desmontado (???) .

Ele também escopo localmente as opções de shell e as restaura no estado em que as encontrou quando o utilitário empacotado retorna. Trata $OPTSespecialmente na medida em que mantém uma cópia em seu próprio escopo à qual atribui inicialmente o valor $-. Depois de também manipular todas as atribuições na linha de comando, ele fará um set -$OPTSpouco antes de chamar seu destino de quebra automática. Dessa forma, se você definir -$OPTSna linha de comando, poderá definir as opções de shell do seu destino de quebra automática. Quando o destino retornar, ele irá set +$- -$OPTScom sua própria cópia de $OPTS (que não é afetada pela linha de comando define) e restaurará tudo para o estado original.

Obviamente, não há nada que impeça o chamador de sair de alguma forma explicitamente returrnda função por meio do alvo de quebra ou de seus argumentos. Isso impedirá qualquer restauração / limpeza do estado que, de outra forma, seria tentada.

Para fazer tudo o que é necessário, é preciso evalaprofundar três . Primeiro, ele se envolve em um escopo local, depois, de dentro, lê argumentos, valida-os para nomes de shell válidos e sai com erro se encontrar um que não é. Se todos os argumentos forem válidos e, eventualmente, uma causa command -v "$1"retornar true (lembre-se: $PATHestá vazio neste momento) , evala linha de comando definirá e transmitirá todos os argumentos restantes para o destino de quebra de linha (embora ignore o caso especial de ns- porque isso não seja muito útil, e três evalsegundos de profundidade é mais do que suficiente) .

Basicamente, funciona assim:

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

Há alguns outros redirecionamentos e, e alguns testes estranhas a ver com a forma como algumas conchas colocar cem $-e, em seguida, recusar-se a aceitá-la como uma opção para set (???) , mas todos os seus auxiliares, e principalmente usado apenas para salvar a partir emitindo saída indesejada e similar em casos extremos. E é assim que funciona. Ele pode fazer essas coisas porque configura seu próprio escopo local antes de chamar seu utilitário agrupado em um tal aninhado.

É longo, porque eu tento ter muito cuidado aqui - três evalsé difícil. Mas com isso você pode fazer:

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

still_local global
 global

Indo um passo adiante e persistindo no namespace do escopo local do utilitário agrupado, não deve ser muito difícil. E mesmo como está escrito, ele já define uma $LOCALSvariável para o utilitário agrupado, que é composta apenas por uma lista separada por espaço de todos os nomes que definiu no ambiente do utilitário agrupado.

Gostar:

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

... o que é perfeitamente seguro - $IFSfoi higienizado com seu valor padrão e somente nomes válidos de shell o fazem, a $LOCALSmenos que você o defina na linha de comando. E mesmo que possa haver caracteres glob em uma variável dividida, você também pode definir OPTS=fna linha de comando o utilitário agrupado para proibir sua expansão. Em qualquer caso:

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      

E aqui está a função. Todos os comandos são prefixados com / \para evitar aliasexpansões:

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
}
mikeserv
fonte
Muito esperto! Um padrão similar é usado para conseguir a mesma coisa aqui: github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh
Zac B