bash como remover opções dos parâmetros após o processamento

9

Lembro-me de ter visto em algum lugar um bashscript usando casee shiftpara percorrer a lista de parâmetros posicionais, analisar sinalizadores e opções com argumentos quando os encontra e os remove após a análise para deixar apenas os argumentos simples, que são processados ​​posteriormente pelo restante do roteiro.

Por exemplo, ao analisar a linha de comando cp -R file1 -t /mybackup file2 -f, ele primeiro percorreria os parâmetros, reconheceria que o usuário solicitou a descida nos diretórios -R, especificou o destino -t /mybackupe forçou a cópia -fe os remove da lista de parâmetros, deixando o programa para processar file1 file2como os argumentos restantes.

Mas parece que não consigo me lembrar / descobrir o script que vi sempre. Eu só gostaria de poder fazer isso. Venho pesquisando em vários sites e anexo uma lista de páginas relevantes que examinei.

Uma pergunta neste site perguntou especificamente sobre "opções independentes de ordem", mas a resposta única e a resposta da pergunta para a qual foi enganada não considera casos como o acima, onde as opções são misturadas com argumentos normais, que eu presumo ser o razão para a pessoa mencionar especificamente opções independentes de pedidos .

Como basho built-in getoptsparece parar no primeiro argumento de não opção, ele não parece ser suficiente como solução. É por isso que a página do Wooledge BashFAQ (veja abaixo) explica como reorganizar os argumentos. Mas eu gostaria de evitar a criação de várias matrizes, caso a lista de argumentos seja bastante longa.

Como shiftnão oferece suporte à exclusão de argumentos individuais no meio da lista de parâmetros, não tenho certeza de qual é uma maneira direta de implementar o que estou pedindo.

Gostaria de saber se alguém tem alguma solução para remover argumentos do meio da lista de parâmetros sem criar uma matriz totalmente nova.

Páginas que eu já vi:

jamadagni
fonte
1
Quando preciso de algo complicado, mudo do bash para o Perl.
choroba 18/09/14

Respostas:

9

POSIXIAMENTE, a análise de opções deve parar no --primeiro argumento de não-opção (ou não-opção-argumento), o que ocorrer primeiro. Então em

cp -R file1 -t /mybackup file2 -f

Essa é, pelo file1, então cprecursivamente deve copiar todos file1, -t, /mybackupe file2para o -fdiretório.

getopt(3)No entanto, o GNU (que o GNU cpusa para analisar opções (e aqui você está usando o GNU cpdesde que está usando a -topção específica do GNU )), a menos que a $POSIXLY_CORRECTvariável de ambiente esteja definida, aceita opções após argumentos. Portanto, é realmente equivalente à análise do estilo de opção POSIX:

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

O getoptsshell embutido, mesmo no shell GNU ( bash), lida apenas com o estilo POSIX. Também não suporta opções longas ou opções com argumentos opcionais.

Se você deseja analisar as opções da mesma maneira que o GNU cp, precisará usar a getopt(3)API do GNU . Para isso, no Linux, você pode usar o getoptutilitário aprimorado de util-linux( essa versão aprimorada do getoptcomando também foi portada para alguns outros Unices como o FreeBSD ).

Isso getoptreorganizará as opções de maneira canônica, permitindo que você as analise simplesmente com um while/caseloop.

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

Você costuma usá-lo 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 $*"

Observe também que isso getoptanalisará as opções da mesma maneira que o GNU cp. Em particular, ele suporta as opções longas (e sua inserção abreviada) e honra as $POSIXLY_CORRECTvariáveis ​​de ambiente (que quando configuradas desativam o suporte a opções após argumentos) da mesma forma que o GNU cp.

Observe que usar o gdb e imprimir os argumentos getopt_long()recebidos pode ajudar a criar os 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",
[...]

Então você pode usar getoptcomo:

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

Lembre-se de que cpa lista de opções suportadas do GNU pode mudar de uma versão para a próxima e que getoptnão poderá verificar se você passa um valor legal para a --sparseopção, por exemplo.

Stéphane Chazelas
fonte
@ all: obrigado por suas respostas. @ Stephane: existe uma razão para você estar usando while [ "$#" -gt 0 ]apenas io while (($#))? É apenas para evitar um basismo?
Jamadagni
Sim, porém, (($#))é mais um kshismo .
Stéphane Chazelas
1

Portanto, toda vez que getoptsprocessa um argumento, ele não espera que defina a variável shell $OPTINDcomo o próximo número na lista de argumentos que deve processar e retorne diferente de 0. Se $OPTINDfor definido com o valor 1, getoptsserá especificado pelo POSIX para aceitar uma variável. nova lista de argumentos. Portanto, isso apenas observa o getoptsretorno, salva incrementos de um contador de mais $OPTINDpor qualquer retorno com falha, muda os argumentos com falha e redefine $OPTINDtodas as tentativas com falha. Você pode usá-lo como opts "$@"- embora queira personalizar o caseloop ou salvá-lo em uma variável e alterar essa seção para 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

Durante a execução, ele define $argstodos os argumentos que getoptsnão trataram ... então ...

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"

RESULTADO

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

Isso funciona em bash, dash, zsh, ksh93, mksh... bem, eu parar de tentar naquele ponto. Em todas as conchas também tem $[Rtf]flage $targ. O ponto é que todos os números dos argumentos que getoptsnão desejavam processar permaneceram.

Alterar o estilo das opções também não fez diferença. Funcionou como -Rf -t/mybackupou -R -f -t /mybackup. Funcionou no meio da lista, no final da lista ou no início da lista ...

Ainda assim, a melhor maneira é colocar um --final de opções na sua lista de argumentos e, em seguida, executar shift "$(($OPTIND-1))"no final de uma getoptsexecução de processamento. Dessa forma, você remove todos os parâmetros processados e mantém o final da lista de argumentos.

Uma coisa que gosto de fazer é traduzir opções longas para curtas - e faço isso de maneira muito semelhante, e é por isso que essa resposta veio com facilidade - antes de executar 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
fonte
A conversão de opções longas também converteria as não opções (como cp -t --file fooou cp -- --file foo) e não lidaria com as opções digitadas abreviadas ( --fi...) ou com a --target=/destsintaxe.
Stéphane Chazelas
@ StéphaneChazelas - a longa conversão é apenas um exemplo - obviamente não foi construída muito bem. Substituí a getoptscoisa por uma função muito mais simples.
mikeserv
@ StéphaneChazelas - por favor, olhe novamente. Embora o longo comentário sobre as opções permaneça justificado, o primeiro - e seu voto negativo - não é mais, como eu penso. Além disso, acho que opts()tem alguma influência em sua própria resposta, pois opts()funciona em um único loop - tocando cada argumento apenas uma vez - e o faz de maneira confiável (o mais próximo que posso dizer) , de maneira portável e sem um único subconjunto.
precisa saber é o seguinte
redefinir OPTIND é o que chamo de iniciar outro loop de getopts. Com efeito, que é a análise de várias linhas de comando / conjuntos de opções ( -R, -t /mybackup, -f). Vou manter meu voto negativo por enquanto, ainda está ofuscado, você está usando $anão inicializado e args= opts...é provável que fique argsdesabilitado (ou definido como era antes) depois de optsretornos em muitas conchas (incluindo o bash).
Stéphane Chazelas
@ StéphaneChazelas - isso inclui bash- eu odeio isso. Na minha opinião, se a função é uma função shell atual que deve ser mantida. Estou abordando essas questões - já que a função atual, com mais testes, poderia ser robusta para lidar com todos os casos e até sinalizar argumentos com sua opção anterior, mas discordo que ela esteja ofuscada . Como está escrito, é o meio mais simples e mais direto de realizar sua tarefa em que consigo pensar. testing shifting faz pouco sentido quando, em todos os shiftcasos com falha , você deveria return. Não se destina a fazer o contrário.
precisa saber é o seguinte