Bash: Cómo eliminar una coma al final de una variable generada por un bucle

Bash: Cómo eliminar una coma al final de una variable generada por un bucle

Tengo el siguiente script bash que genera dos números aleatorios en la misma línea.

#!/bin/bash

for i in 1 2; do
   unset var
   until [ "$var" -lt 10000 ] 2>/dev/null; do
      var="$RANDOM"
done
printf "%s," "${var/%,/}"
done

La salida es:

5751,2129,

Estoy tratando de deshacerme de la coma al final de $varla cual probé "${var/%,/}"para que la salida de $var = 5751,2129 pueda usarse. ¿Alguien puede ayudarme con esto?

Respuesta1

Después de asignar así: var="$RANDOM", la varvariable contiene una cadena de la expansión de $RANDOM. En Bash $RANDOMse expande a un número decimal desde el rango de 0hasta 32767. No hay ningún ,personaje allí, por lo que no tiene sentido intentar eliminarlo ,de la expansión $varposterior.

Los caracteres de coma que observó en el resultado provienen de esta parte del código:

printf "%s," "${var/%,/}"
# here    ^

Este comando se llamó en cada iteración del bucle, por lo que cada iteración agregó un carácter de coma a la salida.

Lo que se ha impreso no se puede desimprimir. Hay matices:

  • Puede canalizar la salida a un filtro y el filtro puede eliminar algunas partes del mismo. El filtro puede estar fuera del script (por ejemplo, llamas the_script | the_filter); o puedes canalizar la salida de algunospartedel script a un filtro dentro del script. En el último caso se filtrará la salida de todo el script; aun así, lo que ha sido impreso por la parte de la escritura no queda sin imprimir; lo digo en seriohacellegar al filtro. El filtro lo elimina más tarde.
  • Si imprime en una terminal, es posible hacer que sobrescriba algunas partes de la salida anterior con datos nuevos. Hay personajes y secuencias para mover el cursor; Aún así todos llegan a la terminal. Puede ocultar visualmente la salida anterior casi de inmediato, pero si redirige la salida a un archivo (o a un terminal que no comprende las secuencias utilizadas), encontrará que todo está allí.

La forma correcta de deshacerse de la coma no deseada es, en primer lugar, no imprimirla; o para filtrarlo dentro del script. Hay varias formas de hacerlo y no es mi intención encontrarlas todas. Hablaré de algunos de ellos. Supongo que desea conocer posibles enfoques también para bucles con más de dos iteraciones; tal vez bucles for donde el número de iteraciones no se conoce de antemano; tal vez bucles for no enumerados por números; tal vez para bucles que quizás nunca terminen (por ejemplo, while trueen lugar de for).

Nota: usó printf "%s," "${var/%,/}", no imprime ningún carácter de nueva línea al final. Intentaré replicar este comportamiento si es posible.

Algunos enfoques posibles:

  1. El interior de tu bucle no depende de $i. Puede deshacerse del bucle y utilizar dos variables separadas:

    unset var1
    until [ "$var1" -lt 10000 ] 2>/dev/null; do
       var1="$RANDOM"
    done
    unset var2
    until [ "$var2" -lt 10000 ] 2>/dev/null; do
       var2="$RANDOM"
    done
    printf '%s,%s' "$var1" "$var2"
    

    Notas:

    • Que no esSECO.
    • No escala bien. (¿Y si lo hubieras hecho for i in {1..100}?)
    • Resulta engorroso si no se conoce de antemano el número de iteraciones.
  2. Puede poner su código actual en una tubería y filtrar la coma final. Ejemplo:

    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done | sed 's/,$//'
    

    Notas:

    • sed(o cualquier filtro que utilice) puede o no manejar la línea incompleta (una línea que no termina con el carácter de nueva línea) que produce el bucle. En caso de sedque dependa de la implementación.
    • Si sedlo maneja, aún puede terminar su salida con una nueva línea.
    • sed(o cualquier filtro que utilice) puede no manejar una línea que sea demasiado larga. El código particular anterior genera una línea razonablemente corta, pero en general (imagine muchas iteraciones) la longitud puede ser un problema.
    • sed, como herramienta orientada a líneas, debe leer una línea completa antes de procesarla. En este caso debe leer toda su entrada. No obtendrá nada de él hasta que finalicen todas las iteraciones.
    • El bucle se ejecuta en una subcapa. En un caso general, es posible que desee que actúe en el shell principal, por cualquier motivo.
  3. Puede capturar el resultado de su código actual en una variable. Al final, elimina la coma final mientras expande la variable:

    capture="$(for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done)"
    printf "%s" "${capture%,}"
    

    Notas:

    • El bucle se ejecuta en una subcapa. En un caso general, es posible que desee que actúe en el shell principal, por cualquier motivo.
    • El código permanece en silencio hasta que llega al último printf(fuera del bucle). No obtendrás nada hasta que finalicen todas las iteraciones.
    • En general, un bucle puede generar cualquier número de bytes. Creo que Bash puede almacenar una cantidad significativa de datos en una variable; entonces, como printfes incorporado, puedeprobablementemanejar printf "%s" "${capture%,}"sin golpearel límite de longitud de una línea de comando. No he probado esto a fondo porque, en mi opinión, almacenar una gran cantidad de datos en una variable de shell no es la mejor práctica de todos modos. Aun así, la práctica puede estar justificada si sabes que el resultado tiene una duración limitada. (Para que conste: el código anterior genera una salida muy breve con seguridad).
    • Bash no puede almacenar caracteres NUL en una variable (la mayoría de los shells no pueden; zsh sí). Además, $()elimina todas las nuevas líneas finales. Esto significa que no puede utilizar una variable para almacenar unarbitrariogenerarlo y reproducirlo más tarde con precisión. (Para que conste: en el código anterior, el fragmento interior $()no genera NUL ni nuevas líneas finales).
  4. En lugar de capturar la salida, puedes hacer que cada iteración se agregue a alguna variable:

    capture=''
    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    capture="$capture$var,"
    done
    printf "%s" "${capture%,}"
    

    Notas:

    • El código se ejecuta en el shell principal (no en un subshell).
    • Aún se aplican las limitaciones de almacenamiento de datos en una variable (consulte el método anterior).
    • En Bash puedes agregar a la variable con capture+="$var,". (Nota: si el atributo de número entero se ha establecido para la variable, entonces=+significa "añadir", no "añadir".)
  5. Puede detectar la última iteración y utilizar un formato sin ,:

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 5 ]; then
          printf "%s" "$var"
       else 
          printf "%s," "$var"
       fi
    done
    

    Notas:

    • Sin subcapa.
    • Detectar la última iteración es más difícil si no se conoce el número de antemano.
    • Es aún más difícil si itera sobre una matriz (por ejemplo for i in "${arr[@]}").
    • Cada iteración se imprime inmediatamente y el resultado se obtiene de forma secuencial. Esto funcionaría incluso si el bucle fuera infinito.
  6. Puede detectar la primera iteración y utilizar un formato sin ,. Tenga en cuenta que podría haber utilizado ,%sen lugar de %s,en su código original; entonces obtendrías ,5751,2129en lugar de 5751,2129,. Con este cambio, cualquiera de los métodos anteriores que evita o elimina la coma final se puede convertir en un método que evita o elimina la coma inicial. El último método se convierte en:

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 1 ]; then
          printf "%s" "$var"
       else 
          printf ",%s" "$var"
       fi
    done
    

    Notas:

    • Sin subcapa.
    • Detectar la primera iteración es fácil si siempre comienza con 1(o con una cadena única fija en general).
    • Pero es más difícil si itera sobre una matriz, por ejemplo for i in "${arr[@]}". No debes verificar if [ "$i" = "${arr[1]}" ]porque puede haber un elemento idéntico al "${arr[1]}"siguiente en la matriz. Una forma sencilla de lidiar con esto es mantener un índice para el bucle ( index=1antes del bucle, luego incrementarlo en uno al final de cada iteración) y probar su valor 1; Sin embargo, ese código me parece algo engorroso.
    • Cada iteración se imprime inmediatamente y el resultado se obtiene de forma secuencial. Esto funcionaría incluso si el bucle fuera infinito.
  7. Puedes hacer ,que provenga de una variable. Ingrese al bucle con la variable vacía y configúrelo ,al final de cada iteración. Esto efectivamente cambiará el valor solo una vez al final delprimeroiteración. Ejemplo:

    # this example is more educative with more than two iterations
    separator=''
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       printf "%s%s" "$separator" "$var"
       separator=,
    done
    

    Notas:

    • Sin subcapa.
    • Funciona bien incluso si se itera sobre una matriz.
    • Cada iteración se imprime inmediatamente y el resultado se obtiene de forma secuencial. Esto funcionaría incluso si el bucle fuera infinito.

    Personalmente encuentro este método bastante elegante.

Notas generales:

  • Cada fragmento genera una salida sin una nueva línea final (con una posible excepción del que tiene sedu otro filtro). Si necesita que toda la salida forme una línea de texto terminada correctamente, ejecute printf '\n'(o sole echo) después del bucle.
  • Quieres ,ser un separador, no un terminador. Esto significa que un bucle con exactamente cero iteraciones generará la misma salida vacía que un bucle con exactamente una iteración, si $varen la iteración se expande a una cadena vacía. En nuestro caso, $varse expande a una cadena no vacía cada vez y sabemos que tenemos más de cero iteraciones; pero en un caso general el uso de un separador en lugar de un terminador puede generar ambigüedad.

información relacionada