Expect脚本递归删除SFTP目录超时问题的排查与解决
问题
我写了一段Expect脚本,用来递归删除SFTP服务器上的目录结构,预期实现:
- 通过while循环遍历目录结构
- 删除最深层目录内的所有文件
- 删除空的最深层目录
- 切换到父目录重复操作
脚本代码如下:
#!/usr/bin/expect set sftp_host [lindex $argv 0] set sftp_user [lindex $argv 1] set sftp_password [lindex $argv 2] set target_dir [lindex $argv 3] set log_file [lindex $argv 4] proc log_message {message log_file} { exec echo "[exec date \"+%Y-%m-%d %H:%M:%S\"] - $message" >> $log_file } log_user 0 exp_internal 0 spawn sftp $sftp_user@$sftp_host expect { -re ".*password:.*" { send "$sftp_password\r" } timeout { log_message "Timeout during login" $log_file exit 1 } } expect { -re ".*sftp.*>" { send "cd $target_dir\r" expect { -re ".*Couldn't canonicalize: No such file or directory.*" { log_message "Directory not found: $target_dir, skipping to next" $log_file send "bye\r" exit 0 } -re ".*sftp.*>" { send "ls -l\r" } } } } if {[llength $expect_out(buffer)] == 0} { log_message "No output from SFTP command for $target_dir" $log_file exit 0 } set dir_stack [list $target_dir] while {[llength $dir_stack] > 0} { set current_dir [lindex $dir_stack end] log_message "Current directory: $current_dir" $log_file set dir_stack [lrange $dir_stack 0 end-1] #log_message "Remaining stack: $dir_stack" $log_file expect { -re ".*sftp.*>" { log_message "Sending command: cd $current_dir" $log_file send "cd $current_dir\r" expect { -re ".*sftp.*>" { ; log_message "Successfully changed directory to: $current_dir" $log_file send "ls -l\r" expect { -re ".*sftp.*>" { set captured_output $expect_out(buffer) log_message "First captured output: $captured_output" $log_file regsub -all {\r} $captured_output "" clean_output set output_lines [split $clean_output "\n"] set clean_lines [lrange $output_lines 1 end] set final_output [join $clean_lines "\n"] log_message "Final captured output: $final_output" $log_file set skip_delete 0 foreach line [split $final_output "\n"] { if {[regexp {^[d-]} $line]} { set entry [lindex [split $line] end] set type [string index $line 0] log_message "entry: $entry" $log_file log_message "type: $type" $log_file if {$type == "d"} { set sub_dir "$current_dir/$entry" log_message "Found subdirectory: $sub_dir" $log_file lappend dir_stack $sub_dir log_message "Remaining stack in foreach: $dir_stack" $log_file set skip_delete 1 } else { log_message "Found file: $entry in $current_dir" $log_file send "rm $entry\r" expect { -re ".*sftp.*>" {} timeout { log_message "Failed to delete file: $entry" $log_file } } } } } if {$skip_delete == 0} { log_message "Deleting directory: $current_dir" $log_file send "cd [file dirname $current_dir]\r" expect -re ".*sftp.*>" send "rmdir [file tail $current_dir]\r" expect { -re ".*sftp.*>" { log_message "Successfully deleted directory: $current_dir" $log_file } timeout { log_message "Failed to delete directory: $current_dir" $log_file } } } } } } } } } } send "bye\r" log_message "Session closed" $log_file exit 0
测试时发现脚本能识别子目录,但识别后后续的cd、ls逻辑卡住,会话超时。
问题原因
- Expect缓冲区未及时清空:每次执行命令后,
expect_out(buffer)会保留之前的输出内容,后续的expect操作会优先匹配旧缓冲区里的内容,而不是等待SFTP返回新的提示符,导致逻辑阻塞。 - 嵌套Expect的逻辑混乱:在处理文件删除的循环里,发送
rm命令后虽然等待了提示符,但没有清理缓冲区,导致后续处理目录时,expect无法正确捕获新的命令输出。 - 栈遍历逻辑顺序错误:当前是先弹出目录再处理,但在发现子目录时将其加入栈,不过因为缓冲区的问题,后续切换目录的命令发送后,无法正确匹配新的提示符,直接陷入超时等待。
解决方法
- 每次Expect后清空缓冲区:在完成一次命令交互后,手动清空
expect_out(buffer),确保后续匹配只针对最新输出。 - 简化嵌套Expect结构:减少不必要的嵌套,改用统一的过程处理等待提示符的逻辑。
- 调整栈遍历逻辑:采用深度优先遍历,先收集所有子目录加入栈,确保先处理最深层目录,再回溯处理父目录。
修改后的脚本如下:
#!/usr/bin/expect set sftp_host [lindex $argv 0] set sftp_user [lindex $argv 1] set sftp_password [lindex $argv 2] set target_dir [lindex $argv 3] set log_file [lindex $argv 4] proc log_message {message log_file} { exec echo "[exec date \"+%Y-%m-%d %H:%M:%S\"] - $message" >> $log_file } # 统一等待SFTP提示符并清空缓冲区 proc wait_for_prompt {} { expect -re ".*sftp.*>" set expect_out(buffer) "" } log_user 0 exp_internal 0 spawn sftp $sftp_user@$sftp_host expect { -re ".*password:.*" { send "$sftp_password\r" wait_for_prompt } timeout { log_message "Timeout during login" $log_file exit 1 } } send "cd $target_dir\r" expect { -re ".*Couldn't canonicalize: No such file or directory.*" { log_message "Directory not found: $target_dir, skipping to next" $log_file send "bye\r" exit 0 } default { wait_for_prompt } } set dir_stack [list $target_dir] while {[llength $dir_stack] > 0} { set current_dir [lindex $dir_stack end] log_message "Processing directory: $current_dir" $log_file # 切换到当前目录 send "cd $current_dir\r" wait_for_prompt # 获取目录内容 send "ls -l\r" expect -re ".*sftp.*>" set captured_output $expect_out(buffer) set expect_out(buffer) "" regsub -all {\r} $captured_output "" clean_output set output_lines [split $clean_output "\n"] set has_subdirs 0 # 先收集所有子目录(跳过.和..),加入栈实现深度优先遍历 foreach line $output_lines { if {[regexp {^d} $line]} { set entry [lindex [split $line] end] if {$entry ni {. ..}} { set sub_dir "$current_dir/$entry" log_message "Adding subdirectory to stack: $sub_dir" $log_file lappend dir_stack $sub_dir set has_subdirs 1 } } } # 无任何子目录时,删除当前目录下所有文件并删除空目录 if {!$has_subdirs} { send "ls -l\r" expect -re ".*sftp.*>" set file_list_output $expect_out(buffer) set expect_out(buffer) "" regsub -all {\r} $file_list_output "" clean_file_output foreach line [split $clean_file_output "\n"] { if {[regexp {^-} $line]} { set file [lindex [split $line] end] log_message "Deleting file: $current_dir/$file" $log_file send "rm $file\r" wait_for_prompt } } # 切换到父目录并删除当前空目录 set parent_dir [file dirname $current_dir] send "cd $parent_dir\r" wait_for_prompt set dir_name [file tail $current_dir] send "rmdir $dir_name\r" expect { -re ".*sftp.*>" { log_message "Successfully deleted directory: $current_dir" $log_file } timeout { log_message "Failed to delete directory: $current_dir (may not be empty)" $log_file } } # 从栈中移除已处理的目录 set dir_stack [lrange $dir_stack 0 end-1] } else { # 存在子目录时,继续处理栈中的子目录,当前目录留到子目录处理完后再处理 continue } } send "bye\r" log_message "Session closed" $log_file exit 0
修改说明:
- 新增
wait_for_prompt过程,统一处理等待提示符和清空缓冲区的操作,避免旧输出干扰后续匹配。 - 调整栈遍历逻辑,先收集所有子目录加入栈,确保先处理最深层目录,实现正确的深度优先递归。
- 跳过
.和..目录,避免循环处理父目录或当前目录。 - 拆分文件删除和目录删除逻辑,只有当目录无任何子目录时才执行删除操作。
内容的提问来源于stack exchange,提问作者YOUNGKYUN OH
相关产品推荐
相关产品推荐

