Minha abordagem seria esta:
Executar comando como processo em segundo plano 1
Execute o "watchdog timer" como processo em segundo plano 2
Configure um manipulador para interceptar um sinal de terminação no shell pai
Aguarde a conclusão de ambos os processos. O processo que termina primeiro envia o sinal de término ao pai.
O manipulador de interceptação dos pais mata os dois processos em segundo plano por meio do controle de tarefas (um deles já foi finalizado por definição, mas essa interrupção será uma operação inofensiva porque não estamos usando PIDs, veja abaixo)
Tentei contornar a possível condição de corrida abordada nos comentários usando os IDs de controle de trabalho do shell (que seriam inequívocos nessa instância do shell) para identificar os processos em segundo plano a serem eliminados, em vez dos PIDs do sistema.
#!/bin/sh
TIMEOUT=$1
COMMAND='sleep 5'
function cleanup {
echo "SIGTERM trap"
kill %1 %2
}
trap cleanup SIGTERM
($COMMAND; echo "Command completed"; kill $$) &
(sleep $TIMEOUT; echo "Timeout expired"; kill $$) &
wait
echo "End of execution"
Resultado para TIMEOUT=10
(o comando termina antes do cão de guarda):
$ ./timeout.sh 10
Command completed
SIGTERM trap
End of execution
Resultado para TIMEOUT=1
(o watchdog termina antes do comando):
$ ./timeout.sh 1
Timeout expired
SIGTERM trap
End of execution
Resultado para TIMEOUT=5
(watchdog e comando terminam "quase" simultaneamente):
./tst.sh 5
Timeout expired
Command completed
SIGTERM trap
End of execution