3

I am trying to run git add -p from ruby. The problem is that this command displays portions of files and waits for user input, potentially opening the git editor. The regular Kernel methods to execute system commands won't work for this reason. I tried to use open3 and here is what I got so far:

require "open3"
Open3.popen3("\git add -p #{files_to_add.join(" ")}") do |stdin, stdout, stderr, wait_thr|
end

I don't know what to put in the block though, and can't find any clues on the interwebs.

Any ideas how I can solve this problem?

NOTE: I DO NOT want to use ANY gems

EDIT: open3 is not working. I am now experimenting with pty. Here is what I've got:

require "pty"
begin
  PTY.spawn("\git add -p #{files_to_add.join(" ")}") do |r, w, pid|
    begin
      r.each { |line| print line }
    rescue Errno::EIO
    end
  end
rescue PTY::ChildExited => e
  puts "The child process exited!"
end

This code prints the first chunk to patch, but then the "question" git asks (in other words, the standard input prompt) does not appear. I am obviously not printing it in the code above, but I can't seem to find out how to do that. Any ideas?

Robert Audi
  • 8,019
  • 9
  • 45
  • 67
  • 2
    Are you sure you want to use `git add -p` which enables interactive mode? It looks like you are automating/streamlining some workflow here. Perhaps there are `git` commands and options available so you can circumvent interactive mode. I don't know the context of your script, but it might be worth a dive into the git manual. – zwippie Dec 15 '13 at 18:00
  • Yes I'm sure I want to use `git add -p` as I am creating a wrapper around that command for one of my personal projects. I'm fiddling with `pty` at the moment, seems like I'm getting somewhere. – Robert Audi Dec 15 '13 at 18:06
  • If you spawn a new terminal, methinks print in the context of that block would apply to it: you're literally printing to a terminal that is not being shown. I suspect the first approach is more likely to succeed. I'm not familiar enough with Open3 to give you the answer, but my guess is you'll want to write to stdin as needed, possibly after collectinginput using gets or something convoluted like that. In other news, there is a git gem that I imagine manages this kind of stuff. So perhaps look into its code for the answer? – Denis de Bernardy Dec 15 '13 at 19:53
  • Why do you wish to use a gem? – Малъ Скрылевъ Dec 15 '13 at 19:54
  • What kind of problem do you want to solve with your wrapper? I'm just curious, as I really cannot think of a problem I would solve by explicitly running the interactive version of a command just to automate the interaction completely afterwards. – michas Dec 15 '13 at 21:14
  • I'm not sure, but why can't you just use backticks or `%x{command}`? I tried it on windows with `pause` command, and it waited for input. – Darek Nędza Dec 15 '13 at 21:32

1 Answers1

3

Inside the pty standard library module (no gems needed here) is an inner module you can require called expect. It will add an expect method to IO.

You probably want something like this:

require 'pty'
require 'expect'

PTY.spawn "git add -p" do |r, w, pid|
  w.sync = true
  r.expect ']? ' do |got|
    puts got
    puts 'responding with q'
    w.write "q\r"
    puts r.expect "\n", 9
  end
end
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
  • 1
    That works impressively well. But I have a couple of questions: 1. `puts got` works but `print got` doesn't. Why? 2. Your code only covers one iteration (or hunk). On what should I loop to go through all hunks? 3. Writing `y` instead of `q`, a hunk should have been staged, but it didn't. Why? – Robert Audi Dec 15 '13 at 21:31
  • 1
    **1.** `IO#expect` returns an `Array` which `IO#puts` handles more gracefully than `Kernel#print` does. **2.** I guess you should loop unconditionally and call `expect(s, 5)` (5 second timeout) and then bail when you finally hit a timeout, unless you can detect the end with expect. **3.** As a guess I would say you need another expect after the `y` because otherwise the exit may kill the git process before it can read and respond to the input you gave it. Without that you certainly have a race condition even if it usually works. – DigitalRoss Dec 15 '13 at 21:45
  • 1
    BTW, you don't have to use a block with `#expect` ... you can just unconditionally call `w.write` which might make sense in your case. Be sure to consider using `#expect`'s optional timeout parameter. – DigitalRoss Dec 15 '13 at 21:54
  • I almost made it work the way I want! One more question though: right now `w.write "...\r` actually prints `...` to the screen. It's annoying because in my case `...` is what the user inputed. Is there a way to "write" but not "show" (something like a "send" method or something)? – Robert Audi Dec 15 '13 at 22:12
  • I imagine that's happening because you are printing out what you got back from `#expect`. – DigitalRoss Dec 15 '13 at 22:47