Función bash con estado

16

Me gustaría implementar una función en Bash que aumente (y devuelva) un recuento con cada llamada. Desafortunadamente, esto no parece trivial ya que estoy invocando la función dentro de una subshell y, en consecuencia, no puede modificar las variables de su shell principal.

Aquí está mi intento:

PS_COUNT=0

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

ps_count_reset() {
    let PS_COUNT=0
}

Esto se usaría de la siguiente manera (y, por lo tanto, mi necesidad de invocar las funciones desde una subshell):

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

De esa manera, tendría un indicador numerado de varias líneas:

> echo 'this
1   is
2   a
3   test'

Linda. Pero debido a la limitación mencionada anteriormente no funciona.

Una solución que no funciona sería escribir el recuento en un archivo en lugar de una variable. Sin embargo, esto crearía un conflicto entre múltiples sesiones que se ejecutan simultáneamente. Por supuesto, podría agregar el ID de proceso del shell al nombre del archivo. Pero espero que haya una mejor solución que no saturará mi sistema con muchos archivos.

Konrad Rudolph
fuente
Las colisiones de WRT con un alijo de archivos se ven man 1 mktemp.
Ricitos
Deberías ver mi edición, creo que te gustará.
mikeserv

Respuestas:

14

ingrese la descripción de la imagen aquí

Para obtener el mismo resultado que anota en su pregunta, todo lo que se necesita es esto:

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

No necesitas contorsionar. Esas dos líneas lo harán todo en cualquier shell que simule algo cercano a la compatibilidad POSIX.

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

Pero me ha gustado esto. Y quería demostrar los fundamentos de lo que hace que esto funcione un poco mejor. Así que edité esto un poco. Lo puse /tmppor ahora, pero creo que también me lo guardaré. Esta aquí:

cat /tmp/prompt

GUIÓN INMEDIATA:

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

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

Nota: habiendo aprendido recientemente de yash , lo construí ayer. Por alguna razón, no imprime el primer byte de cada argumento con la %ccadena, aunque los documentos eran específicos sobre las extensiones de ancho ancho para ese formato y, por lo tanto, pueden estar relacionados, pero funciona bien con%.1s

Eso es todo. Hay dos cosas principales que suceden allí arriba. Y así es como se ve:

/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

Cada vez que $PS1se evalúa, se analiza e imprime $PWDpara agregar a la solicitud. Pero no me gusta que toda $PWDmi pantalla se llene, así que solo quiero la primera letra de cada ruta de navegación en la ruta actual hasta el directorio actual, que me gustaría ver en su totalidad. Me gusta esto:

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

Aquí hay algunos pasos:

IFS=/

Vamos a tener que dividir la corriente $PWDy la forma más confiable de hacerlo es $IFSdividir /. No es necesario molestarse con eso después: toda la división de aquí en adelante se definirá por la $@matriz de parámetros posicionales del shell en el siguiente comando como:

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

Esto es un poco complicado, pero lo principal es que nos estamos dividiendo $PWDen /símbolos. También utilizo la expansión de parámetros para asignar a $lasttodo después de cualquier valor que ocurra entre la /barra diagonal izquierda y la derecha . De esta manera sé que si estoy justo en /y tienen un único /entonces $lasttodavía será igual a la totalidad $PWDy $1estará vacía. Esto importa También me quito $lastel extremo de la cola $PWDantes de asignarlo $@.

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

Entonces, aquí, siempre que ${1+is set}seamos printfel primer %cpersonaje de cada uno de los argumentos de nuestro shell, que acabamos de establecer en cada directorio de nuestro actual $PWD, menos el directorio superior, se divide /. Esencialmente, solo estamos imprimiendo el primer carácter de cada directorio, $PWDexcepto el superior. Sin embargo, es importante darse cuenta de que esto solo sucede si $1se establece, lo que no sucederá en la raíz /o en uno eliminado /, como en /etc.

printf "$last > "

$lastes la variable que acabo de asignar a nuestro directorio superior. Así que ahora este es nuestro directorio principal. Imprime si la última declaración lo hizo o no. Y se necesita un poco limpio >por si acaso.

¿Pero qué hay del incremento?

Y luego está el asunto de lo $PS2condicional. Mostré anteriormente cómo se puede hacer esto, que aún puede encontrar a continuación: esto es fundamentalmente una cuestión de alcance. Pero hay un poco más a menos que desees comenzar a hacer un montón de printf \bespacios y luego tratar de equilibrar su cuenta de personajes ... ugh. Entonces hago esto:

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

De nuevo, ${parameter##expansion}salva el día. Sin embargo, es un poco extraño aquí: en realidad establecemos la variable mientras la despojamos de sí misma. Usamos su nuevo valor, establecer mid-strip, como el glob del que nos despojamos. ¿Lo ves? Nos ##*despojamos de todo de la cabeza de nuestra variable de la subasta hasta el último carácter que puede ser cualquier cosa de [$((PS2c=0))-9]. De esta forma, tenemos la garantía de no generar el valor, y aún así lo asignamos. Es genial, nunca lo había hecho antes. Pero POSIX también nos garantiza que esta es la forma más portátil de hacerlo.

Y es gracias a POSIX-especificado ${parameter} $((expansion))que mantiene estas definiciones en el shell actual sin requerir que las establezcamos en un subshell separado, independientemente de dónde las evaluamos. Y es por eso que funciona en dashy shtan bien como en bashy zsh. No utilizamos escapes dependientes de shell / terminal y dejamos que las variables se prueben a sí mismas. Eso es lo que hace que el código portátil sea rápido.

El resto es bastante simple: solo incremente nuestro contador por cada vez que $PS2se evalúe hasta que $PS1una vez más se restablezca. Me gusta esto:

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

Entonces ahora puedo:

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 igual en basho 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 dije anteriormente, el problema principal es que debes considerar dónde haces tu cálculo. No obtiene el estado en el shell principal, por lo que no calcula allí. Obtiene el estado en la subshell, así que ahí es donde calcula. Pero haces la definición en el shell principal.

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 >
mikeserv
fuente
1
@mikeserv Estamos girando en círculos. Yo se todo esto Pero, ¿cómo uso esto en mi definición de PS2? Esta es la parte difícil. No creo que su solución pueda aplicarse aquí. Si piensas lo contrario, muéstrame cómo.
Konrad Rudolph
1
@mikeserv No, eso no está relacionado, lo siento. Vea mi pregunta para más detalles. PS1y PS2son variables especiales en el shell que se imprimen como símbolo del sistema (pruébelo estableciendo PS1un valor diferente en una nueva ventana del shell), por lo tanto, se usan de manera muy diferente a su código. Aquí hay más información sobre su uso: linuxconfig.org/bash-prompt-basics
Konrad Rudolph
1
@KonradRudolph, ¿qué te impide definirlos dos veces? Que es lo que hizo mi cosa original ... Tengo que mirar tu respuesta ... Esto se hace todo el tiempo.
mikeserv
1
@mikeserv Escriba echo 'thisen el indicador, luego explique cómo actualizar el valor de PS2antes de escribir la comilla simple de cierre.
chepner
1
Bien, esta respuesta ahora es oficialmente sorprendente. También me gustan las migas de pan, aunque no lo adoptaré ya que imprimo
Konrad Rudolph
8

Con este enfoque (función que se ejecuta en una subshell), no podrá actualizar el estado del proceso de shell maestro sin sufrir contorsiones. En su lugar, organice la función para que se ejecute en el proceso maestro.

El valor de la PROMPT_COMMANDvariable se interpreta como un comando que se ejecuta antes de imprimir la PS1solicitud.

por PS2 no hay nada comparable. Pero puede usar un truco en su lugar: dado que todo lo que quiere hacer es una operación aritmética, puede usar la expansión aritmética, que no implica una subshell.

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

El resultado del cálculo aritmético termina en la solicitud. Si desea ocultarlo, puede pasarlo como un subíndice de matriz que no existe.

PS1='${nonexistent_array[$((PS_COUNT=0))]}\$ '
Gilles 'SO- deja de ser malvado'
fuente
4

Es un poco intensivo de E / S, pero necesitará usar un archivo temporal para mantener el valor del recuento.

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

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

Si le preocupa la necesidad de un archivo separado por sesión de shell (lo que parece una preocupación menor; ¿realmente estará escribiendo comandos de varias líneas en dos shells diferentes al mismo tiempo?), Debe usar mktemppara crear un nuevo archivo para cada utilizar.

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"
}
chepner
fuente
+1 La E / S probablemente no sea muy importante ya que si el archivo es pequeño y se accede con frecuencia, se almacenará en caché, es decir, esencialmente funciona como memoria compartida.
Ricitos
1

No puede usar una variable de shell de esta manera y ya comprende por qué. Una subshell hereda variables exactamente de la misma manera que un proceso hereda su entorno: cualquier cambio realizado se aplica solo a él y a sus hijos y no a ningún proceso ancestro.

Según otras respuestas, lo más fácil es guardar esos datos en un archivo.

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

Etc.

encerrada dorada
fuente
Por supuesto, puede establecer una variable de esta manera. No necesita un archivo temporal. Establece la variable en el subshell e imprime su valor en el shell principal donde absorbe ese valor. Obtiene todo el estado que necesita para calcular su valor en el subshell, así que ahí es donde lo hace.
mikeserv
1
@mikeserv Eso no es lo mismo, por eso el OP ha dicho que tal solución no funcionará (aunque esto debería haberse aclarado más en la pregunta). A lo que se refiere es a pasar un valor a otro proceso a través de IPC para que pueda asignar ese valor a lo que sea. Lo que el OP quería / necesitaba hacer era afectar el valor de una variable global compartida por varios procesos, y no puede hacerlo a través del entorno; No es muy útil para IPC.
Ricitos
Hombre, o bien he entendido mal lo que se necesita aquí, o todos los demás lo han hecho. Me parece realmente simple. ¿Ves mi edición? ¿Qué tiene de malo?
mikeserv
@mikeserv No creo que haya entendido mal y, para ser justos, lo que tiene es una forma de IPC y podría funcionar. No me queda claro por qué a Konrad no le gusta, pero si no es lo suficientemente flexible, entonces el almacenamiento de archivos es bastante sencillo (y también lo son las formas de evitar colisiones, por ejemplo mktemp).
Ricitos
2
@mikeserv Se llama a la función deseada cuando PS2el shell expande el valor de . No tiene la oportunidad de actualizar el valor de una variable en el shell principal en ese momento.
chepner
0

Como referencia, aquí está mi solución usando archivos temporales, que son únicos por proceso de shell, y eliminados lo antes posible (para evitar el desorden, como se alude en la pregunta):

# 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"
}
Konrad Rudolph
fuente