Search code examples
bashunixtclexpect

expect fails when running proc inside proc


My script works fine (retrieves sftp prompt) when using one proc. But when I try to use proc inside proc, script gets stuck, and I do not know why.

Please do not refactor the code, that is not the point, I need to understand what is the issue here.

Working code:

proc sftp_connect {} {
  set times 0;
  set connection_retry 2
  set timeout 1;
  while { $times < $connection_retry } {
    spawn sftp ${SFTP_USER}@${SFTP_SERVER}
    expect {
      timeout { puts "Connection timeout"; exit 1}
      default {exit 2}
      "*assword:*" { 
        send "${SFTP_PASSWORD}\n";
        expect {
          "sftp>" { puts "Connected"; set times [ expr $times+1]; exp_continue}
        }
      }
    }
  }
  send "quit\r";
}

sftp_connect

Debug output:

expect: does "\r\nsftp> " (spawn_id exp5) match glob pattern "sftp>"? yes

But after moving send password into separate proc, expect does not retrieve sftp prompt anymore ("sftp>"):

proc sftp_send_password {} {
  send "${SFTP_PASSWORD}\n";
  expect {
    "sftp>" { puts "Connected"; set times [ expr $times+1]; exp_continue}
  }
}

proc sftp_connect {} {
  set times 0;
  set connection_retry 2
  set timeout 1;
  while { $times < $connection_retry } {
    spawn sftp ${SFTP_USER}@${SFTP_SERVER}
    expect {
      timeout { puts "Connection timeout"; exit 1}
      default {exit 2}
      "*assword:*" { sftp_send_password }
    }
  }
  send "quit\r";
}

sftp_connect

Debug output:

expect: does "" (spawn_id exp0) match glob pattern "sftp>"? yes

Solution

  • I don't have my copy of "Exploring Expect" handy, but I think you're running into a variable scoping issue. spawn invisibly sets a variable named spawn_id. When you call spawn in a proc, that variable is scoped only for that proc. Declare it as global:

    proc sftp_connect {} {
      global spawn_id
      # ... rest is the same
    }
    

    I think you don't have to do the same thing in sftp_send_password because expect has a more forgiving scoping scheme than Tcl (if expect does not find a local variable, look in the global namespace).

    Your sftp_send_password proc will not affect the times variable in sftp_connect though, due to the same variable scoping issue. I'd recommend

    proc sftp_send_password {times_var} {
      upvar 1 $times_var times     ;# link this var to that in the caller
      send "${SFTP_PASSWORD}\n";
      expect {
        "sftp>" { puts "Connected"; incr times; exp_continue} 
      }
      # note use of `incr` instead of `expr`
    }
    

    And then the sftp_connect proc sends the times variable name:

    sftp_send_password times