Skip to content

Commit

Permalink
fix: add use_stdin option for just reading from stdin (#547)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrexox authored Sep 13, 2023
1 parent 3ddec2e commit 08a56d3
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 25 deletions.
43 changes: 42 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- [`fail_text`](#fail_text)
- [`stage_fixed`](#stage_fixed)
- [`interactive`](#interactive)
- [`use_stdin`](#use_stdin)
- [Script](#script)
- [`runner`](#runner)
- [`skip`](#skip)
Expand All @@ -51,6 +52,7 @@
- [`fail_text`](#fail_text)
- [`stage_fixed`](#stage_fixed)
- [`interactive`](#interactive)
- [`use_stdin`](#use_stdin)
- [Examples](#examples)
- [More info](#more-info)

Expand Down Expand Up @@ -1045,7 +1047,14 @@ pre-commit:

**Default: `false`**

Whether to use interactive mode and provide a STDIN for a command or script.
Whether to use interactive mode. This applies the certain behavior:
- All `interactive` commands/scripts are executed after non-interactive.
- When executing, lefthook tries to open /dev/tty (Linux/Unix only) and use it as stdin.
- When [`no_tty`](#no_tty) option is set, `interactive` is ignored.

**Note**

If you want to pass stdin to your command or script but don't need to get the input from CLI, use [`use_stdin`](#use_stdin) option isntead.

## Script

Expand Down Expand Up @@ -1093,6 +1102,38 @@ commit-msg:
When you try to commit `git commit -m "bad commit text"` script `template_checker` will be executed. Since commit text doesn't match the described pattern the commit process will be interrupted.

### `use_stdin`

Pass the stdin from the OS to the command/script.

**Note**

With many commands or scripts having `use_stdin: true`, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a [feature request](https://github.com/evilmartians/lefthook/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.md).

**Example**

Use this option for the `pre-push` hook when you have a script that does `while read ...`. Without this option lefthook will hang: lefthook uses [pseudo TTY](https://github.com/creack/pty) by default, and it doesn't close stdin when all data is read.

```bash
# .lefthook/pre-push/do-the-magic.sh
remote="$1"
url="$2"
while read local_ref local_oid remote_ref remote_oid; do
# ...
done
```

```yml
# lefthook.yml
pre-push:
scripts:
"do-the-magic.sh":
runner: bash
use_stdin: true
```

### `runner`

You should specify a runner for the script. This is a command that should execute a script file. It will be called the following way: `<runner> <path-to-script>` (e.g. `ruby .lefthook/pre-commit/lint.rb`).
Expand Down
11 changes: 11 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
- [Concurrent files overrides](#concurrent-files-overrides)
- [Capture ARGS from git in the script](#capture-args-from-git-in-the-script)
- [Git LFS support](#git-lfs-support)
- [Pass stdin to a command or script](#pass-stdin-to-a-command-or-script)
- [Using an interactive command or script](#using-an-interactive-command-or-script)

----

Expand Down Expand Up @@ -304,3 +306,12 @@ Lefthook runs LFS hooks internally for the following hooks:
- pre-push

Errors are suppressed if git LFS is not required for the project. You can use [`LEFTHOOK_VERBOSE`](#lefthook_verbose) ENV to make lefthook show git LFS output.


### Pass stdin to a command or script

When you need to read the data from stdin – specify [`use_stdin: true`](./configuration.md#use_stdin). This option is good when you write a command or script that receives data from git using stdin (for the `pre-push` hook, for example).

### Using an interactive command or script

When you need to interact with user – specify [`interactive: true`](./configuration.md#interactive). Lefthook will connect to the current TTY and forward it to your command's or script's stdin.
1 change: 1 addition & 0 deletions internal/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Command struct {

FailText string `mapstructure:"fail_text" yaml:"fail_text,omitempty" json:"fail_text,omitempty" toml:"fail_text,omitempty"`
Interactive bool `mapstructure:"interactive" yaml:",omitempty" json:"interactive,omitempty" toml:"interactive,omitempty"`
UseStdin bool `mapstructure:"use_stdin" yaml:",omitempty" json:"use_stdin,omitempty" toml:"use_stdin,omitempty"`
StageFixed bool `mapstructure:"stage_fixed" yaml:"stage_fixed,omitempty" json:"stage_fixed,omitempty" toml:"stage_fixed,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions internal/config/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Script struct {

FailText string `mapstructure:"fail_text" yaml:"fail_text,omitempty" json:"fail_text,omitempty" toml:"fail_text,omitempty"`
Interactive bool `mapstructure:"interactive" yaml:",omitempty" json:"interactive,omitempty" toml:"interactive,omitempty"`
UseStdin bool `mapstructure:"use_stdin" yaml:",omitempty" json:"use_stdin,omitempty" toml:"use_stdin,omitempty"`
StageFixed bool `mapstructure:"stage_fixed" yaml:"stage_fixed,omitempty" json:"stage_fixed,omitempty" toml:"stage_fixed,omitempty"`
}

Expand Down
35 changes: 26 additions & 9 deletions internal/lefthook/run/exec/execute_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import (

type CommandExecutor struct{}

type executeArgs struct {
in io.Reader
out io.Writer
envs []string
root string
interactive, useStdin bool
}

func (e CommandExecutor) Execute(opts Options, out io.Writer) error {
in := os.Stdin
if opts.Interactive && !isatty.IsTerminal(os.Stdin.Fd()) {
Expand All @@ -40,10 +48,19 @@ func (e CommandExecutor) Execute(opts Options, out io.Writer) error {
)
}

args := &executeArgs{
in: in,
out: out,
envs: envs,
root: root,
interactive: opts.Interactive,
useStdin: opts.UseStdin,
}

// We can have one command split into separate to fit into shell command max length.
// In this case we execute those commands one by one.
for _, command := range opts.Commands {
if err := e.executeOne(command, root, envs, opts.Interactive, in, out); err != nil {
if err := e.execute(command, args); err != nil {
return err
}
}
Expand All @@ -60,14 +77,14 @@ func (e CommandExecutor) RawExecute(command []string, out io.Writer) error {
return cmd.Run()
}

func (e CommandExecutor) executeOne(cmdstr string, root string, envs []string, interactive bool, in io.Reader, out io.Writer) error {
func (e CommandExecutor) execute(cmdstr string, args *executeArgs) error {
command := exec.Command("sh", "-c", cmdstr)
command.Dir = root
command.Env = append(os.Environ(), envs...)
command.Dir = args.root
command.Env = append(os.Environ(), args.envs...)

if interactive {
command.Stdout = out
command.Stdin = in
if args.interactive || args.useStdin {
command.Stdout = args.out
command.Stdin = args.in
command.Stderr = os.Stderr
err := command.Start()
if err != nil {
Expand All @@ -81,9 +98,9 @@ func (e CommandExecutor) executeOne(cmdstr string, root string, envs []string, i

defer func() { _ = p.Close() }()

go func() { _, _ = io.Copy(p, in) }()
go func() { _, _ = io.Copy(p, args.in) }()

_, _ = io.Copy(out, p)
_, _ = io.Copy(args.out, p)
}

defer func() { _ = command.Process.Kill() }()
Expand Down
30 changes: 19 additions & 11 deletions internal/lefthook/run/exec/execute_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import (
)

type CommandExecutor struct{}
type executeArgs struct {
in io.Reader
out io.Writer
envs []string
root string
}

func (e CommandExecutor) Execute(opts Options, out io.Writer) error {
root, _ := filepath.Abs(opts.Root)
Expand All @@ -22,8 +28,15 @@ func (e CommandExecutor) Execute(opts Options, out io.Writer) error {
)
}

args := &executeArgs{
in: os.Stdin,
out: out,
envs: envs,
root: root,
}

for _, command := range opts.Commands {
if err := e.executeOne(command, root, envs, opts.Interactive, os.Stdin, out); err != nil {
if err := e.execute(command, args); err != nil {
return err
}
}
Expand All @@ -40,22 +53,17 @@ func (e CommandExecutor) RawExecute(command []string, out io.Writer) error {
return cmd.Run()
}

func (e CommandExecutor) executeOne(cmdstr string, root string, envs []string, interactive bool, in io.Reader, out io.Writer) error {
func (e CommandExecutor) execute(cmdstr string, args *executeArgs) error {
cmdargs := strings.Split(cmdstr, " ")
command := exec.Command(cmdargs[0])
command.SysProcAttr = &syscall.SysProcAttr{
CmdLine: strings.Join(cmdargs, " "),
}
command.Dir = root
command.Env = append(os.Environ(), envs...)

if interactive {
command.Stdout = os.Stdout
} else {
command.Stdout = out
}
command.Dir = args.root
command.Env = append(os.Environ(), args.envs...)

command.Stdin = in
command.Stdout = args.out
command.Stdin = args.in
command.Stderr = os.Stderr
err := command.Start()
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions internal/lefthook/run/exec/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (

// Options contains the data that controls the execution.
type Options struct {
Name, Root, FailText string
Commands []string
Env map[string]string
Interactive bool
Name, Root, FailText string
Commands []string
Env map[string]string
Interactive, UseStdin bool
}

// Executor provides an interface for command execution.
Expand Down
2 changes: 2 additions & 0 deletions internal/lefthook/run/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ func (r *Runner) runScript(script *config.Script, path string, file os.FileInfo)
Commands: []string{command},
FailText: script.FailText,
Interactive: script.Interactive && !r.DisableTTY,
UseStdin: script.UseStdin,
Env: script.Env,
}, r.Hook.Follow)

Expand Down Expand Up @@ -374,6 +375,7 @@ func (r *Runner) runCommand(name string, command *config.Command) {
Commands: run.commands,
FailText: command.FailText,
Interactive: command.Interactive && !r.DisableTTY,
UseStdin: command.UseStdin,
Env: command.Env,
}, r.Hook.Follow)

Expand Down

0 comments on commit 08a56d3

Please sign in to comment.