bash 执行带超时的 shell 函数

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9954794/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 21:53:57  来源:igfitidea点击:

Execute a shell function with timeout

bashfunctionshelltimeout

提问by speendo

Why would this work

为什么这行得通

timeout 10s echo "foo bar" # foo bar

but this wouldn't

但这不会

function echoFooBar {
  echo "foo bar"
}

echoFooBar # foo bar

timeout 10s echoFooBar # timeout: failed to run command `echoFooBar': No such file or directory

and how can I make it work?

我怎样才能让它工作?

采纳答案by Douglas Leeder

timeoutis a command - so it is executing in a subprocess of your bash shell. Therefore it has no access to your functions defined in your current shell.

timeout是一个命令 - 所以它在你的 bash shell 的子进程中执行。因此它无法访问您在当前 shell 中定义的函数。

The command timeoutis given is executed as a subprocess of timeout - a grand-child process of your shell.

timeout给出的命令是作为 timeout 的子进程执行的 - 你的 shell 的一个孙子进程。

You might be confused because echois both a shell built-in and a separate command.

您可能会感到困惑,因为echo它既是一个内置的 shell,又是一个单独的命令。

What you can do is put your function in it's own script file, chmod it to be executable, then execute it with timeout.

你可以做的是把你的函数放在它自己的脚本文件中,chmod 它是可执行的,然后用timeout.

Alternatively fork, executing your function in a sub-shell - and in the original process, monitor the progress, killing the subprocess if it takes too long.

或者 fork,在子 shell 中执行您的函数 - 在原始进程中,监视进度,如果花费太长时间则终止子进程。

回答by user3132194

As Douglas Leeder said you need a separate process for timeout to signal to. Workaround by exporting function to subshells and running subshell manually.

正如 Douglas Leeder 所说,您需要一个单独的进程来发出超时信号。解决方法是将函数导出到子外壳并手动运行子外壳。

export -f echoFooBar
timeout 10s bash -c echoFooBar

回答by Eduardo Lago Aguilar

There's an inline alternative also launching a subprocess of bash shell:

还有一个内联替代方案也启动了 bash shell 的子进程:


timeout 10s bash <<EOT
function echoFooBar {
  echo foo
}

echoFooBar
sleep 20
EOT

回答by Tiago Lopo

You can create a function which would allow you to do the same as timeout but also for other functions:

您可以创建一个函数,它允许您执行与超时相同的操作,但也可以用于其他函数:

function run_cmd { 
    cmd=""; timeout="";
    grep -qP '^\d+$' <<< $timeout || timeout=10

    ( 
        eval "$cmd" &
        child=$!
        trap -- "" SIGTERM 
        (       
                sleep $timeout
                kill $child 2> /dev/null 
        ) &     
        wait $child
    )
}

And could run as below:

并且可以运行如下:

run_cmd "echoFooBar" 10

Note: The solution came from one of my questions: Elegant solution to implement timeout for bash commands and functions

注意:该解决方案来自我的一个问题: 实现 bash 命令和函数超时的优雅解决方案

回答by Superole

if you just want to add timeout as an additional option for the entire existing script, you can make it test for the timeout-option, and then make it call it self recursively without that option.

如果您只想为整个现有脚本添加超时作为附加选项,您可以让它测试超时选项,然后让它在没有该选项的情况下递归调用它。

example.sh:

例子.sh:

#!/bin/bash
if [ "" == "-t" ]; then
  timeout 1m 
$./example.sh -other_option # -other_option
                            # YAWN...
else #the original script echo sleep 2m echo YAWN... fi

running this script without timeout:

无超时运行此脚本:

$./example.sh -t -other_option # -other_option

running it with a one minute timeout:

以一分钟的超时运行它:

function foo(){
    for i in {1..100};
    do 
        echo $i;  
        sleep 1;
    done;
}

cat <( foo ) # Will work 
timeout 3 cat <( foo ) # Will Work 
timeout 3 cat <( foo ) | sort # Wont work, As sort will fail 
cat <( timeout 3 cat <( foo ) ) | sort -r # Will Work 

回答by Hemant Patel

## forking is evil
timeout() {
    to=; shift
    $@ & local wp=$! start=0
     while kill -0 $wp; do
        read -t 1
        start=$((start+1))
        if [ $start -ge $to ]; then
            kill $wp && break
        fi
    done
}

回答by untore

This function uses only builtins

此函数仅使用内置函数

  • Maybe consider evaling "$*" instead of running $@ directly depending on your needs

  • It starts a job with the command string specified after the first arg that is the timeout value and monitors the job pid

  • It checks every 1 seconds, bash supports timeouts down to 0.01 so that can be tweaked

  • Also if your script needs stdin, readshould rely on a dedicated fd (exec {tofd}<> <(:))

  • Also you might want to tweak the kill signal (the one inside the loop) which is default to -15, you might want -9

  • 也许根据您的需要考虑评估“$*”而不是直接运行 $@

  • 它使用作为超时值的第一个 arg 之后指定的命令字符串启动作业并监视作业 pid

  • 它每 1 秒检查一次,bash 支持低至 0.01 的超时,以便可以进行调整

  • 此外,如果您的脚本需要 stdin,read则应依赖专用的 fd( exec {tofd}<> <(:))

  • 此外,您可能想要调整默认为 的终止信号(循环内的信号)-15,您可能想要-9

timeout_child () {
    trap -- "" SIGTERM
    child=$!
    timeout=
    (
            sleep $timeout
            kill $child
    ) &
    wait $child
}

回答by TauPan

Putting my comment to Tiago Lopo's answer into more readable form:

将我对 Tiago Lopo 的回答的评论变成更易读的形式:

I think it's more readable to impose a timeout on the most recent subshell, this way we don't need to eval a string and the whole script can be highlighted as shell by your favourite editor. I simply put the commands after the subshell with evalhas spawned into a shell-function (tested with zsh, but should work with bash):

我认为在最近的子 shell 上施加超时更易读,这样我们就不需要评估字符串,并且整个脚本可以由您最喜欢的编辑器突出显示为 shell。我只是将命令放在子 shell 之后eval生成了一个 shell 函数(用 zsh 测试,但应该与 bash 一起使用):

 print_dots () {
     while true
     do
         sleep 0.1
         echo -n .
     done
 }


 > print_dots & timeout_child 2
 [1] 21725
 [3] 21727
 ...................[1]    21725 terminated  print_dots
 [3]  + 21727 done       ( sleep $timeout; kill $child; )

Example usage:

用法示例:

( while true; do echo -n .; sleep 0.1; done) & timeout_child 2

( while true; do echo -n .; sleep 0.1; done) & timeout_child 2

And this way it also works with a shell function (if it runs in the background):

这样它也可以与 shell 函数一起使用(如果它在后台运行):

function timeout_cmd { 
  local arr
  local cmd
  local timeout

  arr=( "$@" )

  # timeout: first arg
  # cmd: the other args
  timeout="${arr[0]}"
  cmd=( "${arr[@]:1}" )

  ( 
    eval "${cmd[@]}" &
    child=$!

    echo "child: $child"
    trap -- "" SIGTERM 
    (       
      sleep "$timeout"
      kill "$child" 2> /dev/null 
    ) &     
    wait "$child"
  )
}

回答by CristianCantoro

I have a slight modification of @Tiago Lopo's answer that can handle commands with multiple arguments. I've also tested TauPan's solution, but it does not work if you use it multiple times in a script, while Tiago's does.

我对@Tiago Lopo 的答案略有修改,可以处理带有多个参数的命令。我还测试了 TauPan 的解决方案,但如果您在脚本中多次使用它,则它不起作用,而 Tiago 的解决方案。

$ ./test_timeout.sh -h
Usage:
  test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT]
  test_timeout.sh -h

Test timeout_cmd function.

Options:
  -n              Dry run, do not actually sleep. 
  -r REPEAT       Reapeat everything multiple times [default: 1].
  -s SLEEP_TIME   Sleep for SLEEP_TIME seconds [default: 5].
  -t TIMEOUT      Timeout after TIMEOUT seconds [default: no timeout].

Here's a fully functional script thant you can use to test the function above:

这是一个功能齐全的脚本,您可以使用它来测试上述功能:

$ ./test_timeout.sh -r 2 -s 5 -t 3
Try no: 1
  - Set timeout to: 3
child: 2540
    -> retval: 143
    -> The command timed out
Try no: 2
  - Set timeout to: 3
child: 2593
    -> retval: 143
    -> The command timed out
Done!

For example you cnal launch like this:

例如,您可以像这样启动:

#!/usr/bin/env bash

#shellcheck disable=SC2128
SOURCED=false && [ "
$ TMOUT=10 && echo "foo bar"
" = "$BASH_SOURCE" ] || SOURCED=true if ! $SOURCED; then set -euo pipefail IFS=$'\n\t' fi #################### helpers function check_posint() { local re='^[0-9]+$' local mynum="" local option="" if ! [[ "$mynum" =~ $re ]] ; then (echo -n "Error in option '$option': " >&2) (echo "must be a positive integer, got $mynum." >&2) exit 1 fi if ! [ "$mynum" -gt 0 ] ; then (echo "Error in option '$option': must be positive, got $mynum." >&2) exit 1 fi } #################### end: helpers #################### usage function short_usage() { (>&2 echo \ "Usage: test_timeout.sh [-n] [-r REPEAT] [-s SLEEP_TIME] [-t TIMEOUT] test_timeout.sh -h" ) } function usage() { (>&2 short_usage ) (>&2 echo \ " Test timeout_cmd function. Options: -n Dry run, do not actually sleep. -r REPEAT Reapeat everything multiple times [default: 1]. -s SLEEP_TIME Sleep for SLEEP_TIME seconds [default: 5]. -t TIMEOUT Timeout after TIMEOUT seconds [default: no timeout]. ") } #################### end: usage help_flag=false dryrun_flag=false SLEEP_TIME=5 TIMEOUT=-1 REPEAT=1 while getopts ":hnr:s:t:" opt; do case $opt in h) help_flag=true ;; n) dryrun_flag=true ;; r) check_posint "$OPTARG" '-r' REPEAT="$OPTARG" ;; s) check_posint "$OPTARG" '-s' SLEEP_TIME="$OPTARG" ;; t) check_posint "$OPTARG" '-t' TIMEOUT="$OPTARG" ;; \?) (>&2 echo "Error. Invalid option: -$OPTARG.") (>&2 echo "Try -h to get help") short_usage exit 1 ;; :) (>&2 echo "Error.Option -$OPTARG requires an argument.") (>&2 echo "Try -h to get help") short_usage exit 1 ;; esac done if $help_flag; then usage exit 0 fi #################### utils if $dryrun_flag; then function wrap_run() { ( echo -en "[dry run]\t" ) ( echo "$@" ) } else function wrap_run() { "$@"; } fi # Execute a shell function with timeout # https://stackoverflow.com/a/24416732/2377454 function timeout_cmd { local arr local cmd local timeout arr=( "$@" ) # timeout: first arg # cmd: the other args timeout="${arr[0]}" cmd=( "${arr[@]:1}" ) ( eval "${cmd[@]}" & child=$! echo "child: $child" trap -- "" SIGTERM ( sleep "$timeout" kill "$child" 2> /dev/null ) & wait "$child" ) } #################### function sleep_func() { local secs local waitsec waitsec=1 secs=$(()) while [ "$secs" -gt 0 ]; do echo -ne "$secs3[0K\r" sleep "$waitsec" secs=$((secs-waitsec)) done } command=("wrap_run" \ "sleep_func" "${SLEEP_TIME}" ) for i in $(seq 1 "$REPEAT"); do echo "Try no: $i" if [ "$TIMEOUT" -gt 0 ]; then echo " - Set timeout to: $TIMEOUT" set +e timeout_cmd "$TIMEOUT" "${command[@]}" retval="$?" set -e echo " -> retval: $retval" # check if (retval % 128) == SIGTERM (== 15) if [[ "$((retval % 128))" -eq 15 ]]; then echo " -> The command timed out" fi else echo " - No timeout" "${command[@]}" retval="$?" fi done echo "Done!" exit 0
##代码##

回答by Michel Hua

This one liner will exit your Bash session after 10s

这个 liner 将在 10 秒后退出您的 Bash 会话

##代码##