Função bash stateful


16

Eu gostaria de implementar uma função no Bash que aumenta (e retorna) uma contagem a cada chamada. Infelizmente, isso não parece trivial, pois estou invocando a função dentro de um subshell e, consequentemente, não pode modificar as variáveis ​​do shell pai.

Aqui está a minha tentativa:

PS_COUNT=0

ps_count_inc() {
    let PS_COUNT=PS_COUNT+1
    echo $PS_COUNT
}

ps_count_reset() {
    let PS_COUNT=0
}

Isso seria usado da seguinte maneira (e, portanto, minha necessidade de invocar as funções de um subshell):

PS1='$(ps_count_reset)> '
PS2='$(ps_count_inc)   '

Dessa forma, eu teria um prompt de várias linhas numerado:

> echo 'this
1   is
2   a
3   test'

Fofa. Mas, devido à limitação mencionada acima, não funciona.

Uma solução não útil seria gravar a contagem em um arquivo em vez de em uma variável. No entanto, isso criaria um conflito entre várias sessões em execução simultânea. Eu poderia acrescentar o ID do processo do shell ao nome do arquivo, é claro. Mas espero que exista uma solução melhor que não atrapalhe meu sistema com muitos arquivos.


Colisões WRT usando um stash de arquivo, consulte man 1 mktemp.
Goldilocks

Você deve ver minha edição - acho que você vai gostar.
mikeserv

Respostas:


14

insira a descrição da imagem aqui

Para obter a mesma saída que você anotou na sua pergunta, tudo o que é necessário é o seguinte:

PS1='${PS2c##*[$((PS2c=0))-9]}- > '
PS2='$((PS2c=PS2c+1)) > '

Você não precisa se contorcer. Essas duas linhas farão tudo em qualquer shell que pretenda algo próximo à compatibilidade com POSIX.

- > cat <<HD
1 >     line 1
2 >     line $((PS2c-1))
3 > HD
    line 1
    line 2
- > echo $PS2c
0

Mas gostei disso. E eu queria demonstrar os fundamentos do que torna esse trabalho um pouco melhor. Então eu editei isso um pouco. Fiquei preso /tmppor agora, mas acho que vou mantê-lo para mim também. Está aqui:

cat /tmp/prompt

PROMPT SCRIPT:

ps1() { IFS=/
    set -- ${PWD%"${last=${PWD##/*/}}"}
    printf "${1+%c/}" "$@" 
    printf "$last > "
}

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'
PS2='$((PS2c=PS2c+1)) > '

Nota: tendo aprendido recentemente sobre yash , eu o construí ontem. Por qualquer motivo, ele não imprime o primeiro byte de cada argumento com a %cstring - embora os documentos sejam específicos sobre extensões de caracteres largos para esse formato e, portanto, talvez relacionados - mas funciona bem com%.1s

Essa é a coisa toda. Há duas coisas principais acontecendo lá em cima. E é assim que se parece:

/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 >

ANÁLISE $PWD

Toda vez que $PS1é avaliada, ele analisa e imprime $PWDpara adicionar ao prompt. Mas eu não gosto da $PWDmultidão toda na minha tela, então quero apenas a primeira letra de cada trilha de navegação no caminho atual até o diretório atual, que eu gostaria de ver na íntegra. Como isso:

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cd /
/ > cd ~
/h/mikeserv > 

Existem alguns passos aqui:

IFS=/

teremos que dividir a atual $PWDe a maneira mais confiável de fazer isso é com a $IFSdivisão ativada /. Não há necessidade de se preocupar com isso depois - todas as divisões daqui em diante serão definidas pela $@matriz de parâmetros posicionais do shell no próximo comando, como:

set -- ${PWD%"${last=${PWD##/*/}}"}

Então, este é um pouco complicado, mas o principal é que estamos dividindo $PWDem /símbolos. Também uso a expansão de parâmetros para atribuir a $lasttudo após qualquer valor entre a /barra mais à esquerda e mais à direita . Dessa forma, eu sei que se eu estiver apenas com /apenas um /, $lastainda será igual ao todo $PWDe $1ficará vazio. Isso importa. Também retiro $lasto final da cauda $PWDantes de atribuí-lo $@.

printf "${1+%c/}" "$@"

Então aqui - contanto ${1+is set}que seja printfo primeiro a %ccaracterizar cada um dos argumentos de nosso shell - que acabamos de definir para cada diretório em nosso atual $PWD- menos o diretório superior - nos separamos /. Portanto, estamos basicamente apenas imprimindo o primeiro caractere de cada diretório, $PWDexceto o principal. É importante, no entanto, perceber que isso só acontece se $1for definido, o que não acontecerá na raiz /ou em uma remoção /como a /etc.

printf "$last > "

$lasté a variável que acabei de atribuir ao nosso diretório superior. Então agora este é o nosso diretório principal. Imprime se a última declaração foi ou não. E é preciso muito pouco >para uma boa medida.

Mas e quanto ao aumento?

E depois há a questão do $PS2condicional. Eu mostrei anteriormente como isso pode ser feito, que você ainda encontra abaixo - isso é fundamentalmente uma questão de escopo. Mas há um pouco mais, a menos que você queira começar a fazer um monte de printf \bespaços de ação e tentar equilibrar a contagem de caracteres ... ugh. Então eu faço isso:

PS1='$(ps1)${PS2c##*[$((PS2c=0))-9]}'

Mais uma vez, ${parameter##expansion}salva o dia. É um pouco estranho aqui - na verdade, definimos a variável enquanto a removemos por si mesma. Usamos seu novo valor - definido no meio da faixa - como o globo do qual tiramos a faixa. Entende? Tiramos ##*tudo da cabeça da nossa variável de incremento até o último caractere, que pode ser qualquer coisa [$((PS2c=0))-9]. Dessa forma, garantimos que não emitimos o valor, e ainda assim o atribuímos. É muito legal - eu nunca fiz isso antes. Mas o POSIX também nos garante que esta é a maneira mais portátil de fazer isso.

E é graças ao POSIX especificado ${parameter} $((expansion))que mantém essas definições no shell atual sem exigir que as definamos em um subshell separado, independentemente de onde as avaliamos. E é por isso que funciona em dashe shtão bem quanto em bashe zsh. Não usamos escapes dependentes do shell / terminal e permitimos que as variáveis ​​se testem. É isso que torna o código portátil rápido.

O resto é bastante simples - basta incrementar nosso contador para cada vez que $PS2é avaliado até $PS1que o redefina novamente. Como isso:

PS2='$((PS2c=PS2c+1)) > '

Então agora eu posso:

DASH DEMO

ENV=/tmp/prompt dash -i

/h/mikeserv > cd /etc
/etc > cd /usr/share/man/man3
/u/s/m/man3 > cat <<HERE
1 >     line 1
2 >     line 2
3 >     line $((PS2c-1))
4 > HERE
    line 1
    line 2
    line 3
/u/s/m/man3 > printf '\t%s\n' "$PS1" "$PS2" "$PS2c"
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
    0
/u/s/m/man3 > cd ~
/h/mikeserv >

SH DEMO

Funciona da mesma forma em bashou sh:

ENV=/tmp/prompt sh -i

/h/mikeserv > cat <<HEREDOC
1 >     $( echo $PS2c )
2 >     $( echo $PS1 )
3 >     $( echo $PS2 )
4 > HEREDOC
    4
    $(ps1)${PS2c##*[$((PS2c=0))-9]}
    $((PS2c=PS2c+1)) >
/h/mikeserv > echo $PS2c ; cd /
0
/ > cd /usr/share
/u/share > cd ~
/h/mikeserv > exit

Como eu disse acima, o principal problema é que você precisa considerar onde faz o cálculo. Você não obtém o estado no shell pai - portanto, não computa lá. Você obtém o estado no subshell - e é aí que você calcula. Mas você faz a definição no shell pai.

ENV=/dev/fd/3 sh -i  3<<\PROMPT
    ps1() { printf '$((PS2c=0)) > ' ; }
    ps2() { printf '$((PS2c=PS2c+1)) > ' ; }
    PS1=$(ps1)
    PS2=$(ps2)
PROMPT

0 > cat <<MULTI_LINE
1 > $(echo this will be line 1)
2 > $(echo and this line 2)
3 > $(echo here is line 3)
4 > MULTI_LINE
this will be line 1
and this line 2
here is line 3
0 >

11
@mikeserv Estamos girando em círculos. Eu sei tudo isso Mas como eu uso isso na minha definição de PS2? Esta é a parte complicada. Não acho que sua solução possa ser aplicada aqui. Se você pensa o contrário, por favor, mostre-me como.
Konrad Rudolph

11
@ MikeServ Não, isso não está relacionado, desculpe. Veja minha pergunta para detalhes. PS1e PS2são variáveis ​​especiais no shell que são impressas como prompt de comando (tente definindo PS1um valor diferente em uma nova janela do shell), elas são usadas de maneira muito diferente do seu código. Aqui estão mais algumas informações sobre seu uso: linuxconfig.org/bash-prompt-basics #
Konrad Rudolph

11
@KonradRudolph, o que impede você de defini-los duas vezes? Foi o que minha coisa original fez ... Tenho que olhar para a sua resposta ... Isso é feito o tempo todo.
mikeserv

11
@mikeserv Digite echo 'thisem um prompt e explique como atualizar o valor de PS2antes de digitar a aspas simples de fechamento.
Chepner # 15/14

11
Ok, esta resposta agora é oficialmente incrível. Eu também gosto da farinha de rosca, embora eu não vou adotá-lo desde que eu imprimir o caminho completo em uma linha separada de qualquer maneira: i.imgur.com/xmqrVxL.png
Konrad Rudolph

8

Com essa abordagem (função em execução em um subshell), você não poderá atualizar o estado do processo mestre do shell sem passar por contorções. Em vez disso, organize a função para executar no processo mestre.

O valor da PROMPT_COMMANDvariável é interpretado como um comando que é executado antes de imprimir o PS1prompt.

Pois PS2não há nada comparável. Mas você pode usar um truque: como tudo o que você quer fazer é uma operação aritmética, você pode usar a expansão aritmética, que não envolve um subshell.

PROMPT_COMMAND='PS_COUNT=0'
PS2='$((++PS_COUNT))  '

O resultado da computação aritmética termina no prompt. Se você deseja ocultá-lo, pode passá-lo como um subscrito de matriz que não existe.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '

4

É um pouco intensivo de E / S, mas você precisará usar um arquivo temporário para armazenar o valor da contagem.

ps_count_inc () {
   read ps_count < ~/.prompt_num
   echo $((++ps_count)) | tee ~/.prompt_num
}

ps_count_reset () {
   echo 0 > ~/.prompt_num
}

Se você estiver preocupado com a necessidade de um arquivo separado por sessão do shell (o que parece ser uma preocupação menor; você realmente digitará comandos de várias linhas em dois shells diferentes ao mesmo tempo?), Você deve mktempcriar um novo arquivo para cada usar.

ps_count_reset () {
    rm -f "$prompt_count"
    prompt_count=$(mktemp)
    echo 0 > "$prompt_count"
}

ps_count_inc () {
    read ps_count < "$prompt_count"
    echo $((++ps_count)) | tee "$prompt_count"
}

+1 A E / S provavelmente não é muito significativa, pois se o arquivo for pequeno e estiver sendo acessado com frequência, ele será armazenado em cache, ou seja, está funcionando essencialmente como memória compartilhada.
Goldilocks

1

Você não pode usar uma variável de shell dessa maneira e já entende o porquê. Um subshell herda variáveis ​​exatamente da mesma maneira que um processo herda seu ambiente: quaisquer alterações feitas se aplicam apenas a ele e a seus filhos e não a nenhum processo ancestral.

Conforme outras respostas, a coisa mais fácil a fazer é esconder esses dados em um arquivo.

echo $count > file
count=$(<file)

Etc.


Claro que você pode definir uma variável dessa maneira. Você não precisa de um arquivo temporário. Você define a variável no subshell e imprime seu valor no shell pai, onde absorve esse valor. Você obtém todo o estado necessário para calcular seu valor no subshell, e é aí que você o faz.
mikeserv

11
@mikeserv Isso não é a mesma coisa, e é por isso que o OP disse que essa solução não funcionará (embora isso deva ter sido mais claro na pergunta). O que você está se referindo é passar um valor para outro processo via IPC para que ele possa atribuir esse valor a qualquer coisa. O que o OP queria / precisava fazer era afetar o valor de uma variável global compartilhada por vários processos, e você não pode fazer isso através do ambiente; não é muito útil para IPC.
Goldilocks

Cara, ou eu não entendi completamente o que é necessário aqui, ou todo mundo tem. Parece realmente simples para mim. Você vê minha edição? O que há de errado com isso?
mikeserv

@ MikeServ Eu não acho que você tenha entendido mal e para ser justo, o que você tem é uma forma de IPC e pode funcionar. Não está claro para mim por que o Konrad não gosta, mas se não for flexível o suficiente, o estoque de arquivos é bastante simples (e também são maneiras de evitar colisões, por exemplo mktemp).
precisa

2
@mikeserv A função pretendida é chamada quando o valor de PS2é expandido pelo shell. Você não tem a oportunidade de atualizar o valor de uma variável no shell pai naquele momento.
Chepner # 15/14

0

Para referência, aqui está minha solução usando arquivos temporários, que são exclusivos por processo de shell, e excluídos o mais rápido possível (para evitar confusão, conforme mencionado na pergunta):

# Yes, I actually need this to work across my systems. :-/
_mktemp() {
    local tmpfile="${TMPDIR-/tmp}/psfile-$$.XXX"
    local bin="$(command -v mktemp || echo echo)"
    local file="$($bin "$tmpfile")"
    rm -f "$file"
    echo "$file"
}

PS_COUNT_FILE="$(_mktemp)"

ps_count_inc() {
    local PS_COUNT
    if [[ -f "$PS_COUNT_FILE" ]]; then
        let PS_COUNT=$(<"$PS_COUNT_FILE")+1
    else
        PS_COUNT=1
    fi

    echo $PS_COUNT | tee "$PS_COUNT_FILE"
}

ps_count_reset() {
    rm -f "$PS_COUNT_FILE"
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.