linux expect详解(ssh自动登录,部署)

linux expect详解(ssh自动登录,部署)

前言

之前写博客一直都是用的hexo,支持增量部署,灰常方便。不过最近自动部署插件,貌似出了些问题。每次写好markdown文档,部署到服务器都是自己手动压缩文件,上传到服务器,再解压缩,非常麻烦。

作为一个懒人,肯定要找一个取巧的方式,查看了下ssh命令,正好有个expect命令。

shell脚本基础

在编写ssh自动登陆脚本之前,先说一下shell脚本的基础,此基础不是一些语法什么的,网上到处都是,这里总结了一下shell脚本的运行机制~

shell脚本的运行方式

首先要说一下shell的几种启动方式,正是踩了脚本启动的坑,才使用原来十分钟就搞定的脚本,花了两个小时才搞定。同时也使得我们运行shell,知其所以然。

  1. 通过文件名执行

    shell脚本可以直接通过文件名执行,需要注意的是文件需要执行权限。通过 sudo chmod +x ./file_name.sh来给文件添加执行权限;

  2. 指定脚本解释器来执行文件

    我们常用的 sh file_name.sh 就是指定了脚本解释器/bin/sh来解释执行脚本;常见的脚本解释器还有:/bin/bash等,我们可以使用ls -l /bin/*sh命令来查看当前可用的脚本解释器;

  3. 使用. ./file_name或source命令执行脚本

    这种方式不会像前两种方式一样fork一个子进程去执行脚本,而是使用当前shell环境执行,用于 .bashrc或者.bash_profile被修改的时候,我们不必重启shell或者重新登录系统,就能使当前的更改生效。

shebang

我们写一个shell脚本时,总是习惯在最前面加上一行 #!/binbash,它就是脚本的shebang,至于为什么叫这么个奇怪的名字,C语言和Unix的开发者丹尼斯·里奇称它为可能是类似于”hash-bang”的英国风描述性文字;

贴一段wiki上的解释:

在计算机科学中,Shebang是一个由井号和叹号构成的字符串行,其出现在文本文件的第一行的前两个字符。 在文件中存在Shebang的情况下,类Unix操作系统的程序载入器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有Shebang的文件路径作为该解释器的参数。

简单的说,它指示了此脚本运行时的解释器,所以,使用文件名直接执行shell脚本时,必须带上shebang; 此外,我们还可以在shebang后面直接附加选项,执行时我们默认使用选项执行;

test.sh的shebang为 #!/bin/sh -x,那我们执行脚本时:

1
./test.sh hello

相当于:

1
bin/sh -x ./test.sh hello;

而编写一个ssh自动部署脚本,需要用到的shebang(解释器)为 /usr/bin/expect;

需要注意的是:在指定脚本解释器来执行脚本时,shebang会被指定的脚本解释器覆盖,即优先使用指定的脚本解释器来执行脚本(习惯性地用sh ./test.sh却提示command not found)

expect解释器

expect是一个能实现自动和交互式任务的解释器,它也能解释常见的shell语法命令,其特色在以下几个命令:

  • send:用于向进程发送字符串
  • expect:从进程接收字符串
  • spawn:启动新的进程
  • interact:允许用户交互

1. set timeout命令:

set timeout n :设置超时时间,计时单位为秒,timeout -1 为永不超时。

1. send命令

send命令的一般用法为 send “string”,它们会我们平常输入命令一样向命令行输入一条信息,当然不要忘了在string后面添加上 \r 表示输入回车;

1
2
expect1.1> send "hello world\n"
hello world

2. expect命令

expect命令是expect解释器的关键命令,它的一般用法为 expect “string”,即期望获取到string字符串,可在在string字符串里使用 * 等通配符;

1.基础知识

expect命令和send命令正好相反,expect通常是用来等待一个进程的反馈。expect可以接收一个字符串参数,也可以接收正则表达式参数。和上文的send命令结合,现在我们可以看一个最简单的交互式的例子:

1
2
expect "hi\n"
send "hello there!\n"

这两行代码的意思是:从标准输入中等到hi和换行键后,向标准输出输出hello there。

tips: <$expect_out(buffer)>存储了所有对expect的输入,<$expect_out(0,string)>存储了匹配到expect参数的输入。

比如如下程序:

1
2
3
4
expect "hi\n"
send "you typed <$expect_out(buffer)>"
send "but I only expected <$expect_out(0,string)>"

当在标准输入中输入

1
2
test
hi

是,运行结果如下

1
2
3
you typed: test
hi
I only expect: hi

2. 模式-动作

expect最常用的语法是来自tcl语言的模式-动作。这种语法极其灵活,下面我们就各种语法分别说明。

  • 单一分支模式语法:
1
expect "hi" {send "You said hi"}

匹配到hi后,会输出”you said hi”

  • 多分支模式语法:
1
2
3
4
expect "hi" { send "You said hi\n" } \
"hello" { send "Hello yourself\n" } \
"bye" { send "That was unexpected\n" }

匹配到hi,hello,bye任意一个字符串时,执行相应的输出。等同于如下写法:

1
2
3
4
5
6
expect {
"hi" { send "You said hi\n"}
"hello" { send "Hello yourself\n"}
"bye" { send "That was unexpected\n"}
}

3. spawn命令

spawn command命令会fork一个子进程去执行command命令,然后在此子进程中执行后面的命令;spawn后的send和expect命令都是和spawn打开的进程进行交互的。如果没有spawn语句,整个expect就无法进行下去,当然,如果真的不要spawn过程也没有关系,虽然这样就没有办法单独执行,但是这个脚本可以与任何调用它的进程进行交互。

在ssh自动登陆脚本中,我们使用 spawn ssh user_name@ip_str,fork一个子进程执行ssh登陆命令;

1
2
3
4
5
6
7
8
9
10
set timeout -1
spawn ftp ftp.test.com //打开新的进程,该进程用户连接远程ftp服务器
expect "Name" //进程返回Name时
send "user\r" //向进程输入anonymous\r
expect "Password:" //进程返回Password:时
send "123456\r" //向进程输入don@libes.com\r
expect "ftp> " //进程返回ftp>时
send "binary\r" //向进程输入binary\r
expect "ftp> " //进程返回ftp>时
send "get test.tar.gz\r" //向进程输入get test.tar.gz\r

这段代码的作用是登录到ftp服务器ftp ftp.uu.net上,并以二进制的方式下载服务器上的文件test.tar.gz。程序中有详细的注释。

4.interact

interact命令很简单,执行到此命令时,脚本fork的子进程会将操作权交给用户,允许用户与当前shell进行交互,让人在适当的时候干预这个过程了。

比如下载完ftp文件时,仍然可以停留在ftp命令行状态,以便手动的执行后续命令。interact可以达到这些目的。下面的demo在自动登录ftp后,允许用户交互。

1
2
3
4
5
6
spawn ftp ftp.test.com
expect "Name"
send "user\r"
expect "Password:"
send "123456\r"
interact

完成脚本

以下是一个完成版的脚本 test.sh:

1
2
3
4
5
6
7
8
9
#!/usr/bin/expect // 指定shebang
set timeout 3 // 设定超时时间为3秒
spawn ssh user_name@172.***.***.*** // fork一个子进程执行ssh命令
expect "*password*" // 期待匹配到 'user_name@ip_string's password:'
send "my_password\r" // 向命令行输入密码并回车
send "sudo -s\r"
send "cd /data/logs\r" // 帮我切换到常用的工作目录
interact // 允许用户与命令行交互

执行 sudo chmod +x ./test.sh命令给shell脚本添加执行权限;

运行 ./test.sh命令,一键登陆成功!

简单的几个命令,,搭配起来解决了与命令行的交互问题后,很多复杂的功能也不在话下了~

就比如,我后来还写了个自动部署到服务器的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/expect
set timeout 30
cd /Users/zyy/Blog
spawn /usr/bin/tar czvf public.tar public/
expect eof
spawn /usr/bin/scp /Users/zyy/Blog/public.tar username@your.ip.address:/home/ubuntu/
expect eof
set timeout 10
spawn ssh ubuntu@115.159.63.39
expect "*Welcome*"
send "rm -rf Blog/\r"
send "tar zxvf public.tar && mv public/ Blog/ && rm public.tar\r"
expect eof
exit

alias别名

脚本完成了,可是还是有些小瑕疵:

  • 输入./file_name.sh命令太长。。。
  • 只能在脚本目录中才能执行,不然使用绝对路径输出的命令更长。

这里我们想到了linux的alias命令:

alias命令:

alias命令使用方式为 alias alias_name="ori_command",将alias_name设置为ori_command的别名,这样我们输入执行alias_name,就相当于执行了ori_command;

可是,我们会发现,当你关闭当前shell后,再打开一个shell窗口,再使用alias_name,系统提示command not found;

有没有能保持命令的方式呢?编辑bash_profile文件。

bash_profile文件

我们编辑bash_profile文件,此文件会在终端窗口创建的时候首先执行一次,所以可以帮我们再设置一次别名;

执行命令vim ~./bash_profile,在文件内部添加:

1
alias alias_name="/root_dir/../file_name.sh

保存后,再使用 . ~./bash_profilesource ~./bash_profile 在当前脚本执行一遍设置别名命令,完成设置;

这样,我们无论在哪个目录,只要输入alias_name命令,回车,真正的一键登陆!

总结

作为一个程序猿,时刻保持着偷懒意识(当然此偷懒非彼偷懒。。。),在类unix系统中,不要浪费了shell这种神奇的工具,让计算机为我们服务~

坚持原创技术分享,您的支持将鼓励我继续创作!