├── .gitignore ├── renovate.json ├── go.mod ├── .golangci.yml ├── README.md ├── .goreleaser.yml ├── .github └── workflows │ ├── reviewdog.yaml │ ├── test.yaml │ └── release.yaml ├── go.sum ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /ntimes 3 | /pkg 4 | /vendor 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yuya-takeyama/ntimes 2 | 3 | go 1.18 4 | 5 | require github.com/jessevdk/go-flags v1.5.0 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | linters: 5 | enable-all: true 6 | disable: 7 | - gochecknoglobals 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ntimes 2 | 3 | Command to execute command N times 4 | 5 | ## Usage 6 | 7 | ``` 8 | $ ntimes 3 -- echo foo bar baz 9 | foo bar baz 10 | foo bar baz 11 | foo bar baz 12 | ``` 13 | 14 | ### Set parallel degree of execution (-p) 15 | 16 | ``` 17 | $ ntimes 10 -p 3 -- sh -c 'echo "Hi!"; sleep 1; echo "Bye"' 18 | ``` 19 | 20 | ## Author 21 | 22 | Yuya Takeyama 23 | 24 | ## License 25 | 26 | The MIT License 27 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | ldflags: 13 | - -s -w 14 | - -X main.version={{ .Version }} 15 | - -X main.gitCommit={{ .ShortCommit }} 16 | 17 | archives: 18 | - format_overrides: 19 | - goos: windows 20 | format: zip 21 | 22 | checksum: 23 | name_template: 'checksums.txt' 24 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yaml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**.go' 7 | - '.github/workflows/reviewdog.yaml' 8 | 9 | jobs: 10 | golangci-lint: 11 | name: Run golangci-lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Lint 17 | uses: reviewdog/action-golangci-lint@v2 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 2 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 3 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 4 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 5 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= 6 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | paths: 9 | - '**.go' 10 | - 'go.mod' 11 | - 'go.sum' 12 | - '.github/workflows/test.yaml' 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Setup Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.15.x' 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | - name: Test 25 | run: go test -v -race ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup Go 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: '1.15.x' 16 | - name: Run GoReleaser 17 | uses: goreleaser/goreleaser-action@v5 18 | with: 19 | version: latest 20 | args: release --rm-dist 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "sync" 10 | 11 | flags "github.com/jessevdk/go-flags" 12 | ) 13 | 14 | const appName = "ntimes" 15 | 16 | var ( 17 | version = "" 18 | gitCommit = "" 19 | ) 20 | 21 | type options struct { 22 | Parallels int `short:"p" long:"parallels" description:"Parallel degree of execution" default:"1"` 23 | ShowVersion bool `short:"v" long:"version" description:"Show version"` 24 | } 25 | 26 | func main() { 27 | var opts options 28 | parser := flags.NewParser(&opts, flags.Default^flags.PrintErrors) 29 | parser.Name = appName 30 | parser.Usage = "N [OPTIONS] -- COMMAND" 31 | 32 | args, err := parser.Parse() 33 | if err != nil { 34 | if flagsErr, ok := err.(*flags.Error); ok { 35 | if flagsErr.Type == flags.ErrHelp { 36 | parser.WriteHelp(os.Stderr) 37 | 38 | return 39 | } 40 | } 41 | 42 | errorf("flag parse error: %s", err) 43 | os.Exit(1) 44 | } 45 | 46 | if opts.ShowVersion { 47 | _, _ = io.WriteString(os.Stdout, fmt.Sprintf("%s v%s, build %s\n", appName, version, gitCommit)) 48 | 49 | return 50 | } 51 | 52 | cnt, err := strconv.Atoi(args[0]) 53 | cmdName := args[1] 54 | cmdArgs := args[2:] 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | ntimes(cnt, cmdName, cmdArgs, os.Stdin, os.Stdout, os.Stderr, opts.Parallels) 61 | } 62 | 63 | func ntimes(cnt int, cmdName string, cmdArgs []string, stdin io.Reader, stdout io.Writer, stderr io.Writer, parallels int) { 64 | var wg sync.WaitGroup 65 | 66 | sema := make(chan bool, parallels) 67 | 68 | for i := 0; i < cnt; i++ { 69 | wg.Add(1) 70 | 71 | go func() { 72 | sema <- true 73 | 74 | defer func() { 75 | wg.Done() 76 | <-sema 77 | }() 78 | 79 | cmd := exec.Command(cmdName, cmdArgs...) 80 | cmd.Stdin = stdin 81 | cmd.Stdout = stdout 82 | cmd.Stderr = stderr 83 | 84 | err := cmd.Run() 85 | if err != nil { 86 | panic(err) 87 | } 88 | }() 89 | } 90 | 91 | wg.Wait() 92 | close(sema) 93 | } 94 | 95 | func errorf(message string, args ...interface{}) { 96 | subMessage := fmt.Sprintf(message, args...) 97 | _, _ = fmt.Fprintf(os.Stderr, "%s: %s\n", appName, subMessage) 98 | } 99 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func TestSerial(t *testing.T) { 11 | cmd := exec.Command("go", "run", "main.go", "2", "--", "sh", "-c", `echo "Hi!"; sleep 1; echo "Bye"`) 12 | stdout := &bytes.Buffer{} 13 | stderr := &bytes.Buffer{} 14 | cmd.Stdout = stdout 15 | cmd.Stderr = stderr 16 | 17 | expected := "Hi!\nBye\nHi!\nBye\n" 18 | if err := cmd.Run(); err != nil { 19 | t.Errorf("failed: %v", err) 20 | } 21 | 22 | if stdout.String() != expected { 23 | t.Errorf("stdout doesn't match\nExpected:\n%s\nActual:\n%s", expected, stdout.String()) 24 | } 25 | } 26 | func TestParallel(t *testing.T) { 27 | cmd := exec.Command("go", "run", "main.go", "-p", "2", "2", "--", "sh", "-c", `echo "Hi!"; sleep 1; echo "Bye"`) 28 | stdout := &bytes.Buffer{} 29 | stderr := &bytes.Buffer{} 30 | cmd.Stdout = stdout 31 | cmd.Stderr = stderr 32 | 33 | expected := "Hi!\nHi!\nBye\nBye\n" 34 | if err := cmd.Run(); err != nil { 35 | t.Errorf("failed: %v", err) 36 | } 37 | 38 | if stdout.String() != expected { 39 | t.Errorf("stdout doesn't match\nExpected:\n%s\nActual:\n%s", expected, stdout.String()) 40 | } 41 | } 42 | 43 | func TestVersion(t *testing.T) { 44 | cmd := exec.Command("go", "run", "-ldflags", "-X main.version=1.2.3 -X main.gitCommit=deadbeef", "main.go", "--version") 45 | stdout := &bytes.Buffer{} 46 | stderr := &bytes.Buffer{} 47 | cmd.Stdout = stdout 48 | cmd.Stderr = stderr 49 | 50 | expected := "ntimes v1.2.3, build deadbeef\n" 51 | if err := cmd.Run(); err != nil { 52 | t.Errorf("failed: %v", err) 53 | } 54 | 55 | if stdout.String() != expected { 56 | t.Errorf("stdout doesn't match\nExpected:\n%s\nActual:\n%s", expected, stdout.String()) 57 | } 58 | } 59 | 60 | func TestHelp(t *testing.T) { 61 | cmd := exec.Command("go", "run", "main.go", "--help") 62 | stdout := &bytes.Buffer{} 63 | stderr := &bytes.Buffer{} 64 | cmd.Stdout = stdout 65 | cmd.Stderr = stderr 66 | 67 | expected := `Usage: 68 | ntimes N [OPTIONS] -- COMMAND 69 | 70 | Application Options: 71 | -p, --parallels= Parallel degree of execution (default: 1) 72 | -v, --version Show version 73 | 74 | Help Options: 75 | -h, --help Show this help message 76 | ` 77 | if err := cmd.Run(); err != nil { 78 | var exitErr *exec.ExitError 79 | if !errors.As(err, &exitErr) { 80 | t.Errorf("failed: %v", err) 81 | } 82 | } 83 | 84 | if stdout.String() != "" { 85 | t.Errorf("stdout should be empty") 86 | } 87 | 88 | if stderr.String() != expected { 89 | t.Errorf("stderr doesn't match\nExpected: \n%s\nActual:\n%s", expected, stderr.String()) 90 | } 91 | } 92 | 93 | func TestUnknownOption(t *testing.T) { 94 | cmd := exec.Command("go", "run", "main.go", "--foo") 95 | stdout := &bytes.Buffer{} 96 | stderr := &bytes.Buffer{} 97 | cmd.Stdout = stdout 98 | cmd.Stderr = stderr 99 | 100 | expected := "ntimes: flag parse error: unknown flag `foo'\nexit status 1\n" 101 | if err := cmd.Run(); err != nil { 102 | var exitErr *exec.ExitError 103 | if !errors.As(err, &exitErr) { 104 | t.Errorf("failed: %v", err) 105 | } 106 | } 107 | 108 | if stdout.String() != "" { 109 | t.Errorf("stdout should be empty") 110 | } 111 | 112 | if stderr.String() != expected { 113 | t.Errorf("stderr doesn't match\nExpected: \n%s\nActual:\n%s", expected, stderr.String()) 114 | } 115 | } 116 | --------------------------------------------------------------------------------