Skip to content

Allow timeout for Exec to avoid hanging #225

Description

@rcowham

It would be nice to be able to specify a timeout to avoid commands hanging during execution.

Maybe something like the existing WithEnv(), but instead WithTimeout() or WithWaitDelay() to support the new (Go 1.20) WaitDelay?

https://pkg.go.dev/os/exec#Cmd.WaitDelay

Activity

  1. bitfield commented on Mar 13, 2025

    @bitfield
    Owner

    Great suggestions, @rcowham! There's been some previous discussion on this, if you haven't already seen it: in #207 and #105, for example. Would you like to review those and see if you can propose a concrete example program to demonstrate your idea?

    Good feature proposals tend to satisfy as many as possible of the following criteria:

    1. Show that the lack of such a feature is a significant problem for a significant number of users
    2. Propose a feature that addresses the problem in a satisfactory way
    3. Include a runnable example program that shows how the proposed syntax or API would be used to solve the problem for a realistic use case
    4. Show that this is significantly better than any exisitng solution without the feature
  2. rcowham commented on Mar 28, 2025

    @rcowham
    Author

    For my use cases, a timeout on the whole pipeline is sufficient - just want to avoid an indefinite hang. Clearly this can be done by me putting it on a thread, and killing the process if required - but if library can do it...

    err := script.Exec(cmd).WithTimeout(3*time.Second)
    if err == script.Timeout {
       // do something
    } else if err != nil {
        // do something else
    }
    
    s, err := script.Exec(cmd).WithTimeout(3*time.Second).Slice()
    // Similar error handling as above
    
    errbuf := new(bytes.Buffer)
    p := script.NewPipe().WithStderr(errbuf).WithTimeout(3*time.Second)
    result, err := p.Exec(cmd).<other methods>
    // Similar error handling as above
    
  3. bitfield commented on Mar 28, 2025

    @bitfield
    Owner

    A context would be the more idiomatic way to do this, wouldn't it?

  4. rcowham commented on Mar 28, 2025

    @rcowham
    Author

    A context would be fine - wasn't sure from reading other issues if that was the preferred way...

  5. bitfield commented on Apr 5, 2025

    @bitfield
    Owner

    Just a reminder that this is with you (or whoever wants to work on it) to complete the four steps I outlined.

  6. added a commit that references this issue on Oct 25, 2025
    3c1c8be
  7. lazysegtree commented on Dec 26, 2025

    @lazysegtree

    @bitfield I feel that a way to include timeout should be implemented

    (1) Show that the lack of such a feature is a significant problem for a significant number of users

    I am not sure of the userbase of this library, but here is an example of an issues

    Writing a script to continuously monitor dns lookup on a server. DNS lookups can sometimes take longer then 200 msec, which is a bad thing and unwanted that should be logged.

    There could be other cases, like running command that can get stuck in an infinite loop on invalid input, or invalid state of the system, or running other network monitoring commands that needs specific timeout

    (2) Propose a feature that addresses the problem in a satisfactory way

    Option 1 - Allow a new API ExecWithContext(ctx context.Context, cmdLine string) that will stop the execution of the command when ctx is cancelled. We would need to use exec.CommandContext instead of exec.Command, and pass this ctx
    Option 2 - Allow a new API ExecWithTimeout(cmdLine string, timeout time.Time) that will stop the execution of the command timeout is reached. We would need to use exec.CommandContext instead of exec.Command, and pass a new context with timeout

    I prefer option 1

    (3) Include a runnable example program that shows how the proposed syntax or API would be used to solve the problem for a realistic use case

    Original code. (Note : There might be a better way for custom timeout impl)

    Details

    package main
    
    import (
    	"errors"
    	"fmt"
    	"strings"
    	"time"
    
    	"github.com/bitfield/script"
    )
    
    func ExecuteWithTimeout(f func() string, timeout time.Duration) (string, error) {
    	result := make(chan string, 1)
    	go func() {
    		result <- f()
    	}()
    	select {
    	case msg := <-result:
    		return msg, nil
    	case <-time.After(timeout):
    		return "", errors.New("timeout")
    	}
    }
    
    func main() {
    	for {
    		script.File("servers.txt").FilterLine(func(url string) string {
    			res, err := ExecuteWithTimeout(func() string {
    				ip, _ := script.Exec(fmt.Sprintf("dig +short %s", url)).First(1).String()
    				return strings.TrimSpace(ip)
    			}, time.Millisecond*200)
    
    			if err != nil {
    				res = err.Error()
    			}
    			return fmt.Sprintf("[%v] %s -> %s", time.Now(), url, res)
    		}).Stdout()
    		time.Sleep(time.Second)
    	}
    }

    With ctx API, it could be easier

    Options 1 more generic and needs boilerplate context management code

    Details

    package main
    
    import (
    	"context"
    	"fmt"
    	"strings"
    	"time"
    
    	"github.com/bitfield/script"
    )
    
    func main() {
    
    	for {
    		script.File("servers.txt").FilterLine(func(url string) string {
    			ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
    			defer cancel()
    			
    			ip, err := script.ExecWithContext(ctx, fmt.Sprintf("dig +short %s", url)).First(1).String()
    			res := strings.TrimSpace(ip)
    
    			if err != nil {
    				res = err.Error()
    			}
    			return fmt.Sprintf("[%v] %s -> %s", time.Now(), url, res)
    		}).Stdout()
    		time.Sleep(time.Second)
    	}
    }

    Option 2 more specific, and easier to use

    Details

    package main
    
    import (
    	"fmt"
    	"strings"
    	"time"
    
    	"github.com/bitfield/script"
    )
    
    func main() {
    
    	for {
    		script.File("servers.txt").FilterLine(func(url string) string {
    			ip, err := script.ExecWithTimeout(fmt.Sprintf("dig +short %s", url), 200*time.Millisecond).
    				First(1).String() // ExecWithTimeout will create a fresh context and manage it
    			res := strings.TrimSpace(ip)
    
    			if err != nil {
    				res = err.Error()
    			}
    			return fmt.Sprintf("[%v] %s -> %s", time.Now(), url, res)
    		}).Stdout()
    		time.Sleep(time.Second)
    	}
    }

    (4) Show that this is significantly better than any existing solution without the feature

    This should be evident from the two examples.

    I am not sure if I picked the best example for this, but there could be many other real use-cases that would benefit from this feature

  8. bitfield commented on Dec 29, 2025

    @bitfield
    Owner

    @lazysegtree thanks for the very detailed and thoughtful contribution!

    To be honest, while I completely agree that one can imagine cases where a timeout would be useful, there doesn't actually seem to be a large number of users currently wanting this feature. If there were, I think they'd be commenting on this issue.

    It's probably not worth any of us putting a lot of time and effort into designing such a feature if there's no demand for it. Of course, if dozens of people proceed to comment here saying they'd like it, then by all means, let's start thinking!

  9. lazysegtree commented on Dec 29, 2025

    @lazysegtree

    We have @sk91 from #207 , @rew1nter from #105 @ehsundar from #235.
    @rcowham and @lazysegtree from here.

    Lets wait for 7 more.

  10. rew1nter commented on Jan 17, 2026

    @rew1nter

    A timeout feature would be super nice. I think already enough people want it

  11. rcowham commented on Jan 17, 2026

    @rcowham
    Author
  12. billvamva commented on Jul 6, 2026

    @billvamva
    Contributor

    Hey @bitfield , I am keen to get involved in contributing here, is this something I can pick up? I don't see a merged PR yet.

  13. added a commit that references this issue on Jul 30, 2026
  14. bitfield commented on Aug 15, 2026

    @bitfield
    Owner

    Fixed in #244

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions