Obtendo errado $ LINENO para uma função interceptada

6

Estou escrevendo um script Bash para aprender scripts. Em algum momento, preciso adicionar trap para limpar diretórios e arquivos indesejados se o script for eliminado. No entanto, por alguma razão eu não entendo, trap chama a função de limpeza - clean_a() - quando o script é morto mas $LINENO aponta para uma linha na função de limpeza, não na função - archieve_it() - quando o script é morto .

Comportamento esperado:

  1. executar script
  2. Pressione Ctrl + C
  3. armadilha de caches Ctrl + C e chama clean_a() function
  4. clean_a() function ecoa o número da linha, que Ctrl + C é pressionado. Deixe a linha 10 em archieve_it() .

O que realmente acontece:

  1. executar script
  2. Pressione Ctrl + C
  3. armadilha de caches Ctrl + C e chama clean_a() function
  4. clean_a() ecoa um número de linha irrelevante. Diga, linha 25 na função clean_a() .

Aqui está um exemplo como parte do meu script:

archieve_it () {
  trap 'clean_a $LINENO $BASH_COMMAND'\
                SIGHUP SIGINT SIGTERM SIGQUIT
  for src in ${sources}; do
   mkdir -p "${dest}${today}${src}"
   if [[ "$?" -ne 0 ]] ; then
    error "Something!" 
   fi
   rsync "${options}" \
         --stats -i \
         --log-file="${dest}${rsync_log}" \
         "${excludes}" "${src}" "${dest}${today}${src}"
  done
}
clean_a () {
  error "something!
  line: $LINENO
  command: $BASH_COMMAND
  removing ${dest}${today}..."
  cd "${dest}"
  rm -rdf "${today}"
  exit "$1"
}

P.S .: o script original pode ser visto aqui . Definições e nomes de variáveis estão em turco. Se for necessário, posso traduzir qualquer coisa para o inglês.

EDIT: Eu mudo o roteiro da melhor forma que eu posso fazer de acordo com a explicação do @mikeserv assim:

#!/bin/bash
PS4='DEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO $BASH_COMMAND'\
                SIGHUP SIGINT SIGTERM SIGQUIT
  ..
}
clean_a () {
  error " ...
  line: $LINENO $LASTNO
  ..."
}

Agora, se eu executar o script com set -x e terminá-lo com Ctrl + C , imprime o número de linha correto, como pode ser visto abaixo:

 DDEBUG: 1 : clean_a 1 336 rsync '"${options}"' ...

No entanto, na função clean_a() , o valor de $LASTNO é impresso como 1.

 line: 462 1

Isso tem algo a ver com o bug mostrado por @Arkadiusz Drabczyk?

EDIT2 : eu mudei o script da mesma forma que o @mikesrv me recomendou. Mas $ LASTNO retornou 1 como o valor da linha quando o script foi encerrado (deveria ter sido 337).

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
                SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
clean_a () {
  error " ...
  line: $LASTNO $LINENO
  ..."
} 2>&1

Se eu executar o script e terminá-lo com Ctrl + C enquanto o rsync estava em execução, recebo esta saída:

^^MDEBUG: 1 : clean_a '337 1 rsync "${options}" --delete-during ...
...
line: 1 465

Como você pode ver, o valor de $ LASTNO é 1.

Enquanto eu tentava descobrir qual era o problema, escrevi outra função - testing - usando o formato de substituição de parâmetro ${parameter:-default} . Então script acabou assim:

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'testing "$LASTNO $LINENO $BASH_COMMAND"'\
                 SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
testing() {
  echo -e "${1:-Unknown error!}"
  exit 1
} 2>&1

Agora, se eu executar o script e pressionar Ctrl + C , recebo esta saída:

^^MDEBUG: 1 : testing '337 1 rsync "${options}" --delete-during ...
337 1 rsync "${options}" --delete-during ... 

337 pontos fora da linha quando eu pressionei Ctrl + C , enquanto o rsync estava rodando.

Para outro teste, tentei escrever clear_a funtion assim:

clear_a () {
  echo -e " $LASTNO $LINENO"
}

e $ LASTNO ainda retornaram 1.

Então, isso significa que podemos obter um número de linha correto quando o script é encerrado se usarmos a substituição de parâmetro?

EDIT3 Parece que eu apliquei incorretamente a explicação do @mikeserv no EDIT2. Eu corrigi meu erro. O parâmetro posicional "$1 deve ser substituído por $ LASTNO em clear_a funtion.

Aqui está o script que funciona como eu quero que funcione:

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
  trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
                SIGHUP SIGINT SIGTERM SIGQUIT
  ...
} 2>/dev/null
clean_a () {
  error " ...
  line: $1
  ..."
} 2>&1

Quando o script é encerrado, trap avalia $LASTNO - primeiro argumento -, $LINENO - segundo argumento - e $BASH_COMMAND -third argument -, em seguida, passa seus valores para a função clear_a . Por fim, imprimimos $ LASTNO com $1 como o número da linha cujo script foi encerrado.

    
por numand 23.08.2014 / 15:45

2 respostas

4

Acho que o problema é que você espera que "$LINENO" forneça a linha de execução do último comando, o que pode funcionar quase, mas clean_a() também obtém seu próprio $LINENO e que você deve fazer em vez disso:

error "something!
line: $1
...

Mas mesmo isso provavelmente não funcionaria porque espero que seja apenas imprimir a linha na qual você definiu o trap .

Aqui está uma pequena demonstração:

PS4='DEBUG: $LINENO : ' \
bash -x <<\CMD          
    trap 'fn "$LINENO"' EXIT             
    fn() { printf %s\n "$LINENO" "$1"; }
    echo "$LINENO"
CMD

OUTPUT

DEBUG: 1 : trap 'fn "$LINENO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1
DEBUG: 2 : printf '%s\n' 2 1
2
1

Portanto, o trap é definido e, em seguida, fn() é definido e, em seguida, echo é executado. Quando o shell conclui a execução de sua entrada, o EXIT trap é executado e fn é chamado. É passado um argumento - que é o trap da linha $LINENO . fn imprime primeiro seu próprio $LINENO , em seguida, seu primeiro argumento.

Posso pensar em uma maneira de obter o comportamento esperado, mas isso estraga ostderr:

do shell
PS4='DEBUG: $((LASTNO=$LINENO)) : ' \
bash -x <<\CMD
    trap 'fn "$LINENO" "$LASTNO"' EXIT
    fn() { printf %s\n "$LINENO" "$LASTNO" "$@"; }
    echo "$LINENO"
CMD

OUTPUT

DEBUG: 1 : trap 'fn "$LINENO" "$LASTNO"' EXIT
DEBUG: 3 : echo 3
3
DEBUG: 1 : fn 1 3
DEBUG: 2 : printf '%s\n' 2 1 1 3
2
1
1
3

Ele usa o prompt de depuração $PS4 do shell para definir $LASTNO em cada linha executada. É uma variável de shell atual que você pode acessar em qualquer lugar dentro do script. Isso significa que não importa qual linha está sendo acessada atualmente, você pode fazer referência à linha mais recente do script executada em $LASTNO . Claro, como você pode ver, vem com saída de depuração. Você pode enviar isso para 2>/dev/null para a maioria da execução do script, e apenas 2>&1 em clean_a() ou algo assim.

O motivo pelo qual você obtém 1 em $LASTNO é porque esse é o último valor para o qual $LASTNO foi definido porque esse foi o último valor $LINENO . Você tem seu trap na função archieve_it() e, portanto, ele recebe seu próprio $LINENO , como é indicado na especificação abaixo. Embora não pareça que bash faz a coisa certa de qualquer maneira, também pode ser porque o trap tem que reexecutar o shell em INT signal e $LINENO é, portanto, redefinido. Estou um pouco confuso sobre isso neste caso - como é bash , aparentemente.

Você não quer avaliar $LASTNO em clean_a() , eu acho. Melhor seria avaliá-lo no trap e passar o valor trap recebe em $LASTNO até clean_a() como argumento. Talvez assim:

#!/bin/bash
PS4='^MDEBUG: $((LASTNO=$LINENO)) : '; set -x
archieve_it () {
    trap 'clean_a $LASTNO $LINENO "$BASH_COMMAND"' \
        SIGHUP SIGINT SIGTERM SIGQUIT
    while :; do sleep 1; done
} 2>/dev/null
clean_a () { : "$@" ; } 2>&1

Tente isso - ele deve fazer o que você quiser, eu acho. Ah - e note que em PS4=^M o ^M é um retorno literal - como CTRL + V ENTER.

Da especificação de shell POSIX :

Set by the shell to a decimal number representing the current sequential line number (numbered starting with 1) within a script or function before it executes each command. If the user unsets or resets LINENO , the variable may lose its special meaning for the life of the shell. If the shell is not currently executing a script or function, the value of LINENO is unspecified. This volume of IEEE Std 1003.1-2001 specifies the effects of the variable only for systems supporting the User Portability Utilities option.

    
por 23.08.2014 / 18:05
6

A solução de mikeserv é boa, mas ele está incorreto ao dizer que fn é passado na trap da linha $LINENO quando a armadilha é executada. Insira uma linha antes de trap ... e você verá que fn na verdade sempre passou 1 , independentemente de onde a interceptação foi declarada.

PS4='DEBUG: $LINENO : ' \
bash -x <<\EOF
    echo Foo
    trap 'fn "$LINENO"' EXIT             
    fn() { printf %s\n "$LINENO" "$1"; }
    echo "$LINENO"
    exit
EOF

OUTPUT

DEBUG: 1 : echo Foo
Foo
DEBUG: 2 : trap 'fn "$LINENO"' EXIT
DEBUG: 4 : echo 4
4
DEBUG: 5 : exit
DEBUG: 1 : fn 1
DEBUG: 3 : printf '%s\n' 3 1
3
1

Como o primeiro argumento a interceptar, fn "$LINENO" , é colocado dentro de citações únicas , $LINENO é expandido , se e somente quando EXIT é acionado e deve, portanto, se expandir para fn 5 . Então, por que não? Na verdade, até o bash-4.0 foi deliberadamente alterado, de modo que $ LINENO é redefinido para 1 quando o trap é acionado e, portanto, se expande para fn 1 . [source] No entanto, o comportamento original ainda é mantido para armadilhas ERR, provavelmente porque quantas vezes algo como trap 'echo "Error at line $LINENO"' ERR é usado.

#!/bin/bash

trap 'echo "exit at line $LINENO"' EXIT
trap 'echo "error at line $LINENO"' ERR
false
exit 0

OUTPUT

error at line 5
exit at line 1

testado com o GNU bash, versão 4.3.42 (1) -release (x86_64-pc-linux-gnu)

    
por 18.03.2016 / 09:37

Tags