You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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逻辑卡住,会话超时。

问题原因

  1. Expect缓冲区未及时清空:每次执行命令后,expect_out(buffer)会保留之前的输出内容,后续的expect操作会优先匹配旧缓冲区里的内容,而不是等待SFTP返回新的提示符,导致逻辑阻塞。
  2. 嵌套Expect的逻辑混乱:在处理文件删除的循环里,发送rm命令后虽然等待了提示符,但没有清理缓冲区,导致后续处理目录时,expect无法正确捕获新的命令输出。
  3. 栈遍历逻辑顺序错误:当前是先弹出目录再处理,但在发现子目录时将其加入栈,不过因为缓冲区的问题,后续切换目录的命令发送后,无法正确匹配新的提示符,直接陷入超时等待。

解决方法

  1. 每次Expect后清空缓冲区:在完成一次命令交互后,手动清空expect_out(buffer),确保后续匹配只针对最新输出。
  2. 简化嵌套Expect结构:减少不必要的嵌套,改用统一的过程处理等待提示符的逻辑。
  3. 调整栈遍历逻辑:采用深度优先遍历,先收集所有子目录加入栈,确保先处理最深层目录,再回溯处理父目录。

修改后的脚本如下:

#!/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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:14:55