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 parece não ser trivial, já que estou invocando a função dentro de um subshell e, consequentemente, não é possível modificar as variáveis do shell pai.

Esta é 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 numeradas:

> echo 'this
1   is
2   a
3   test'

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

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

    
por Konrad Rudolph 15.04.2014 / 16:08

5 respostas

14

Paraobteromesmoresultadoquevocêanotanasuapergunta,tudooqueénecessárioéoseguinte:

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

Vocênãoprecisasecontorcer.EssasduaslinhasfarãotudoemqualquershellquepretendaalgopróximoàcompatibilidadePOSIX.

->cat<<HD1>line12>line$((PS2c-1))3>HDline1line2->echo$PS2c0

Maseugosteidisso.Eeuqueriademonstrarosfundamentosdoquefazestetrabalhoumpoucomelhor.Entãoeuediteiissoumpouco.Euenfieiem/tmpporenquanto,masachoquevouguardarparamimtambém.Estáaqui:

cat/tmp/prompt

SCRIPTDEPROMPT:

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

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

Observação: tendo recentemente aprendido sobre yash , eu o construí ontem. Por alguma razão, ele não imprime o primeiro byte de cada argumento com a string %c - embora os documentos sejam específicos sobre extensões wide-char para esse formato e talvez estejam relacionados - mas funciona bem com %.1s

Essa é a coisa toda. Há duas coisas principais acontecendo lá em cima. E é assim que 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 >

PARSING $PWD

Every time $PS1 is evaluated it parses and prints $PWD to add to the prompt. But I don't like the whole $PWD crowding my screen, so I want just the first letter of every breadcrumb in the current path down to the current directory, which I'd like to see in full. Like this:

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

There are a few steps here:

IFS=/

we're going to have to split the current $PWD and the most reliable way to do that is with $IFS split on /. No need to bother with it at all afterward - all splitting from here on out will be defined by the shell's positional parameter $@ array in the next command like:

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

So this one's a little tricky, but the main thing is that we're splitting $PWD on / symbols. I also use parameter expansion to assign to $last everything after any value occurring between the left-most and right-most / slash. In this way I know that if I'm just at / and have only one / then $last will still equal the whole $PWD and $1 will be empty. This matters. I also strip $last from the tail end of $PWD before assigning it to $@.

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

So here - as long as ${1+is set} we printf the first %character of each our shell's arguments - which we've just set to each directory in our current $PWD - less the top directory - split on /. So we're essentially just printing the first character of every directory in $PWD but the top one. It's important though to realize this only happens if $1 gets set at all, which will not happen at root / or at one removed from / such as in /etc.

printf "$last > "

$last is the variable I just assigned to our top directory. So now this is our top directory. It prints whether or not the last statement did. And it takes a neat little > for good measure.

MAS O QUE É SOBRE O INCREMENTO?

And then there's the matter of the $PS2 conditional. I showed earlier how this can be done which you can still find below - this is fundamentally an issue of scope. But there's a little more to it unless you want to start doing a bunch of printf \backspaces and then trying to balance out their character count... ugh. So I do this:

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

Again, ${parameter##expansion} saves the day. It's a little strange here though - we actually set the variable while we strip it of itself. We use its new value - set mid-strip - as the glob from which we strip. You see? We ##*strip all from the head of our increment variable to the last character which can be anything from [$((PS2c=0))-9]. We're guaranteed in this way not to output the value, and yet we still assign it. It's pretty cool - I've never done that before. But POSIX also guarantees us that this is the most portable way this can be done.

E é graças ao ${parameter} $((expansion)) especificado pelo POSIX 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 dash e sh tão bem quanto em bash e zsh . Não usamos escapes dependentes de shell / terminal e deixamos as variáveis testarem a si mesmas. Isso é o que torna o código portátil rápido.

O restante é bastante simples - basta incrementar nosso contador para cada vez que $PS2 for avaliado até que $PS1 redefina novamente. Assim:

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

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 bash ou 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 você faz o seu cálculo. Você não obtém o estado no shell pai - assim você não calcula lá. Você obtém o estado na subcamada - 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 >
    
por 15.04.2014 / 16:26
8

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

O valor da variável PROMPT_COMMAND é interperetizado como um comando que é executado antes de imprimir o prompt PS1 .

Para PS2 , não há nada comparável. Mas você pode usar um truque: já que 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 do cálculo aritmético acaba no prompt. Se você quiser escondê-lo, você pode passá-lo como um subscrito de matriz que não existe.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '
    
por 16.04.2014 / 02:02
4

É um pouco intensivo de I / O, mas você precisará usar um arquivo temporário para manter 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ê está preocupado em precisar de um arquivo separado por sessão de shell (o que parece ser uma preocupação menor; você realmente estará digitando comandos de várias linhas em dois shells diferentes ao mesmo tempo?), use mktemp to crie um novo arquivo para cada uso.

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"
}
    
por 15.04.2014 / 17:47
1

Você não pode usar uma variável shell dessa maneira e você já entende o porquê. Um subshell herda variáveis exatamente da mesma maneira que um processo herda seu ambiente: quaisquer mudanças feitas se aplicam somente a ele e seus filhos e não a qualquer processo ancestral.

De acordo com outras respostas, a melhor coisa a fazer é armazenar esses dados em um arquivo.

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

Etc.

    
por 15.04.2014 / 17:49
0

Para referência, aqui está minha solução usando arquivos temporários, que são exclusivos por processo de shell, e excluídos assim que possível (para evitar desordem, como 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"
}
    
por 15.04.2014 / 17:49