bash cómo eliminar las opciones de los parámetros después del procesamiento

9

Recuerdo haber visto en alguna parte una bashsecuencia de comandos que usa casey shiftrecorre la lista de parámetros posicionales, analiza indicadores y opciones con argumentos cuando los encuentra, y los elimina después del análisis para dejar solo los argumentos desnudos, que luego son procesados ​​por el resto del guión.

Por ejemplo, al analizar la línea de comando de cp -R file1 -t /mybackup file2 -f, primero caminaría a través de los parámetros, reconocería que el usuario ha solicitado descender a directorios por -R, especificó el objetivo por -t /mybackupy para forzar la copia -fy eliminará de la lista de parámetros, dejando el programa a procesar file1 file2como los argumentos restantes.

Pero parece que no puedo recordar / descubrir el guión que vi cada vez. Solo me gustaría poder hacer eso. He estado buscando en varios sitios y agrego una lista de páginas relevantes que examiné.

Una pregunta en este sitio web preguntó específicamente sobre "opciones independientes del orden", pero tanto la respuesta única como la respuesta a la pregunta a la que fue duplicada no considera casos como el anterior donde las opciones se mezclan con argumentos normales, lo que supongo que fue razón para que la persona mencione específicamente opciones independientes del pedido .

Como bashla función incorporada getoptsparece detenerse en el primer argumento sin opción, no parece ser suficiente como solución. Es por eso que la página de Wooledge BashFAQ (ver más abajo) explica cómo reorganizar los argumentos. Pero me gustaría evitar crear múltiples matrices en caso de que la lista de argumentos sea bastante larga.

Dado shiftque no admite la aparición de argumentos individuales en el medio de la lista de parámetros, no estoy seguro de cuál es una forma directa de implementar lo que estoy preguntando.

Me gustaría saber si alguien tiene alguna solución para eliminar argumentos del medio de la lista de parámetros sin crear una matriz completamente nueva.

Páginas que ya he visto:

jamadagni
fuente
1
Una vez que necesito algo complicado, cambio de bash a Perl.
choroba

Respuestas:

9

POSIXY, el análisis de las opciones debe detenerse en --el primer argumento sin opción (o sin argumento de opción), lo que ocurra primero. Entonces en

cp -R file1 -t /mybackup file2 -f

Eso es al file1, por lo que cpde forma recursiva deben copiar todos file1, -t, /mybackupy file2en el -fdirectorio.

getopt(3)Sin embargo, GNU (que GNU cpusa para analizar opciones (y aquí está usando GNU cpya que está usando la -topción específica de GNU )), a menos que se establezca la $POSIXLY_CORRECTvariable de entorno, acepta opciones después de argumentos. Por lo tanto, en realidad es equivalente al análisis de estilo de opción POSIX:

cp -R -t /mybackup -f -- file1 file2

El getoptsshell incorporado, incluso en el shell GNU ( bash) solo maneja el estilo POSIX. Tampoco admite opciones largas u opciones con argumentos opcionales.

Si desea analizar las opciones de la misma manera que lo cphace GNU , deberá usar la getopt(3)API de GNU . Para eso, si está en Linux, puede usar la getoptutilidad mejorada de util-linux( esa versión mejorada del getoptcomando también se ha portado a otros Unices como FreeBSD ).

Eso getoptreorganizará las opciones de una manera canónica que le permite analizarlo simplemente con un while/casebucle.

$ getopt -n "$0" -o t:Rf -- -Rf file1 -t/mybackup file2
 -R -f -t '/mybackup' -- 'file1' 'file2'

Normalmente lo usarías como:

parsed_options=$(
  getopt -n "$0" -o t:Rf -- "$@"
) || exit
eval "set -- $parsed_options"
while [ "$#" -gt 0 ]; do
  case $1 in
    (-[Rf]) shift;;
    (-t) shift 2;;
    (--) shift; break;;
    (*) exit 1 # should never be reached.
  esac
done
echo "Now, the arguments are $*"

También tenga en cuenta que getoptanalizará las opciones de la misma manera que lo cphace GNU . En particular, admite las opciones largas (y al ingresarlas abreviadas) y respeta las $POSIXLY_CORRECTvariables de entorno (que, cuando se configuran, desactivan el soporte para las opciones después de los argumentos) de la misma manera que lo cphace GNU .

Tenga en cuenta que usar gdb e imprimir los argumentos que getopt_long()recibe puede ayudar a construir los parámetros para getopt(1):

(gdb) bt
#0  getopt_long (argc=2, argv=0x7fffffffdae8, options=0x4171a6 "abdfHilLnprst:uvxPRS:T", long_options=0x417c20, opt_index=0x0) at getopt1.c:64
(gdb) set print pretty on
(gdb) p *long_options@40
$10 = {{
    name = 0x4171fb "archive",
    has_arg = 0,
    flag = 0x0,
    val = 97
  }, {
    name = 0x417203 "attributes-only",
[...]

Entonces puedes usarlo getoptcomo:

getopt -n cp -o abdfHilLnprst:uvxPRS:T -l archive... -- "$@"

Recuerde que cpla lista de opciones compatibles de GNU puede cambiar de una versión a la siguiente y eso getoptno podrá verificar si pasa un valor legal a la --sparseopción, por ejemplo.

Stéphane Chazelas
fuente
@todos: gracias por sus respuestas. @Stephane: ¿hay alguna razón por la que estás usando while [ "$#" -gt 0 ]io just while (($#))? ¿Es solo para evitar un bashismo?
jamadagni
Sí, aunque (($#))es más un kshismo .
Stéphane Chazelas
1

Por lo tanto, cada vez que getoptsprocesa un argumento, no espera que establezca la variable de shell $OPTINDen el siguiente número en la lista de argumentos que debe procesar y devuelve otro que no sea 0. Si $OPTINDse establece en un valor de 1, getoptsse especifica POSIX para aceptar un nueva lista de argumentos Por lo tanto, esto solo observa el getoptsretorno, guarda los incrementos de un contador más $OPTINDpara cualquier retorno fallido, aleja los argumentos fallidos y restablece $OPTINDcada intento fallido. Puede usarlo como opts "$@"si quisiera personalizar el casebucle o guardarlo en una variable y cambiar esa sección a eval $case.

opts() while getopts :Rt:f opt || {
             until command shift "$OPTIND" || return 0
                   args=$args' "${'"$((a=${a:-0}+$OPTIND))"'}"'
                   [ -n "${1#"${1#-}"}" ]
             do OPTIND=1; done 2>/dev/null; continue
};     do case "$opt"  in
             R) Rflag=1      ;;
             t) tflag=1      ;
                targ=$OPTARG ;;
             f) fflag=1      ;;
       esac; done

Mientras se ejecuta, establece $argstodos los argumentos que getoptsno manejan ... así que ...

set -- -R file1 -t /mybackup file2 -f
args= opts "$@"; eval "set -- $args"
printf %s\\n "$args"
printf %s\\n "$@"         
printf %s\\n "$Rflag" "$tflag" "$fflag" "$targ"

SALIDA

 "${2}" "${5}"
file1
file2
1
1
1
/mybackup

Esto funciona bash, dash, zsh, ksh93, mksh... bueno, dejar de tratar en ese punto. En cada caparazón también consiguió $[Rtf]flagy $targ. El punto es que todos los números para los argumentos que getoptsno quisieron procesar permanecieron.

Cambiar el estilo de opciones tampoco hizo ninguna diferencia. Funcionó como -Rf -t/mybackupo -R -f -t /mybackup. Funcionó en el medio de la lista, al final de la lista, o al principio de la lista ...

Aún así, la mejor manera es simplemente pegar un --final de opciones en su lista de argumentos y luego hacerlo shift "$(($OPTIND-1))"al final de una getoptsejecución de procesamiento. De esa manera, elimina todos los parámetros procesados y mantiene el final de la lista de argumentos.

Una cosa que me gusta hacer es traducir las opciones largas a cortas, y lo hago de una manera muy similar, por eso esta respuesta fue fácil, antes de correr getopts.

i=0
until [ "$((i=$i+1))" -gt "$#" ]
do case "$1"                   in
--Recursive) set -- "$@" "-R"  ;;
--file)      set -- "$@" "-f"  ;;
--target)    set -- "$@" "-t"  ;;
*)           set -- "$@" "$1"  ;;
esac; shift; done
mikeserv
fuente
La conversión de opciones largas también convertiría las no opciones (como cp -t --file fooo cp -- --file foo) y no haría frente a las opciones ingresadas abreviadas ( --fi...), o con la --target=/destsintaxis.
Stéphane Chazelas
@ StéphaneChazelas: la conversión larga es solo un ejemplo, obviamente no se ha desarrollado muy bien. Reemplacé la getoptscosa con una función mucho más simple.
mikeserv
@ StéphaneChazelas - por favor mira de nuevo. Si bien el comentario de la opción larga permanece justificado, el primero, y su voto negativo que lo acompaña, ya no lo es, según creo. Además, creo que opts()tiene algo que ver con su propia respuesta, ya que opts()funciona en un solo bucle, tocando cada argumento pero una vez, y lo hace de manera confiable (lo más cerca que puedo decir) , de forma portátil y sin una sola subshell.
mikeserv
restablecer OPTIND es lo que llamo iniciar otro bucle getopts. En efecto, eso es analizar varias líneas de comandos / conjuntos de opciones ( -R, -t /mybackup, -f). Mantendré mi voto negativo por ahora, ya que todavía está ofuscado, lo está usando sin $ainicializar y args= opts...es probable que deje sin argsconfigurar (o establecer lo que era antes) después de optsregresar en muchos shells (que incluye bash).
Stéphane Chazelas
@ StéphaneChazelas - eso incluye bash- Odio eso. En mi opinión, si la función es una función de shell actual que debería conservarse. Estoy abordando estas cosas, ya que la función actual, con más pruebas, se podría hacer de manera robusta para manejar todos los casos e incluso para marcar el argumento con su opción anterior, pero no estoy de acuerdo con que esté ofuscado . Tal como está escrito, es el medio más simple y directo de cumplir su tarea que se me ocurre. testing entonces shifting tiene poco sentido cuando en cada shiftcaso fallido deberías return. No tiene la intención de hacer lo contrario.
mikeserv