前言:
今天给大家分享一篇在我工作中常用的一个shell脚本,里面有一些我们常用到的shell操作。该脚本用于本地电脑和服务器交互上,实现以下功能:
- 自动拉取自己个人电脑上的源码到服务器上yocto包的源码文件夹。
- 自动运行compile 、strip
- 自动把编译好的结果回传到个人电脑上。
- 拷贝执行文件到机器中
作者:良知犹存
转载授权以及围观:欢迎关注微信公众号:羽林君
或者添加作者个人微信:become_me
情节介绍:
在工作中,我们经常会遇到需要把修改的代码放到服务器去编译,然后把编译好的文件放到机器板卡对应的文件。这个过程如果我们使用命令的话,大概有七八条,完成操作完之后 ,大约会花费一分。虽然花费的时间不算多,但是你也会一直关注终端,然后等待输入命令,需要花费不小精力。但是我们可以写一些shell脚本来辅助我们的工作。今天就是给大家分享使用的脚本,一边分享使用过程,一边分享里面用到的shell技术点。包含,if判断、switch case条件、字符截取、远程调用脚本、两种免密登陆等。
三者的使用过程为:本地通过脚本输入参数选择使用wifi分配的ip还是网线分配的ip,然后进行ip数据传参形式 远程调用预先放置好的——服务器编译脚本,服务器脚本通过传入的ip参数,免密scp拷贝本地的文件到服务器执行目录,服务器执行编译工作,然后拷贝到本地的指定目录,最后调用本地的expect免密登陆的脚本,拷贝文件到机器中。
脚本一:本地选择脚本auto_build.sh
脚本内容如下:
#/bin/bash remote_ip=172.160.111.32 remote_hostname=lyn case $1 in 1) echo -e '\033[0;42m Ethernet dhcp \033[0m' VAR="eno1" ;; 2) echo -e '\033[0;46m wireless dhcp \033[0m' VAR="wlo1" ;; esac HOST_IP=$(ifconfig $VAR | grep "inet" | grep -v inet6| awk '{ print $2}' | awk -F: '{print $1}') echo "parse ip is:" $HOST_IP if [[ ! -n "${HOST_IP}" ]] ;then echo -e "\033[0;31m input local ip \033[0m" read local_ip else if [[ ! $(echo "${HOST_IP}" | awk -F. '{printf $1}') == "192" ]] ;then local_ip=$HOST_IP else echo -e "\033[0;31m error ip \033[0m" exit 0 fi fi #exit 0 if [[ $2 == 1 ]];then build_opt="all_build" else build_opt= fi ssh -t ${remote_hostname}@${remote_ip} "/home/lyn/build.sh ip=${local_ip} ${build_opt}"
这个部分有几处技术使用:
switch case使用,if else、免密登陆,远程调用脚本。
首先是一个switch case
此处作用是进行ip地址的筛选,因为的在调试过程中,我的电脑有时候用网线连接,有时候会去测试房去测试,用wifi连接,这个时候会进行网络ip地址的区分,当我输入./auto_build.sh 1的时候,脚本会进行解析eno1网线分配的ip地址,当我输入./auto_build.sh 2的时候则会解析wlo1wifi分配的ip。
在里面我还用了颜色打印,进行关键词的标注,如下所示
关于颜色打印的部分这个是另一个知识,这是一个转义的实际使用过程,通过特定符号的转义识别,我们在Linux终端去显示不同颜色的打印输出,这个是我们经常使用的操作,例如log等级分级打印时候,error是红色,正常是绿色,普通是白色等。
颜色打印大致介绍如下:
转义序列以控制字符’ESC’开头。该字符的ASCII码十进制表示为27,十六进制表示为0x1B,八进制表示为033。多数转义序列超过两个字符,故通常以’ESC’和左括号’['开头。该起始序列称为控制序列引导符(CSI,Control Sequence Intro),通常由 ‘\033[’ 或 ‘\e[’ 代替。
通过转义序列设置终端显示属性时,可采用以下格式:
\033[ Param {;Param;...}m 或 \e[ Param {;Param;...}m
其中,’\033[‘或’\e['引导转义序列,'m’表示设置属性并结束转义序列。
因此,通过转义序列设置终端显示属性时,常见格式为:
\033[显示方式;前景色;背景色m输出字符串\033[0m 或\e[显示方式;前景色;背景色m输出字符串\033[0m
其中 ,’\033[0m’用于恢复默认的终端输出属性,否则会影响后续的输出。
示例:我在此处使用 echo -e ‘\033[0;42m Ethernet dhcp \033[0m’ 进行网线端口ip分配的打印,通过转义之后,打印颜色为带背景色的绿色显示。具体对应的颜色,大家可以看一下小麦老兄写的这篇文章 printf打印还能这么玩。
注:打印log时候记得echo 要使用 -e参数。
其次还有组合使用命令实现获取本地ip
HOST_IP=$(ifconfig $VAR | grep "inet" | grep -v inet6| awk '{ print $2}' | awk -F: '{print $1}')
我们一步步查看执行情况
第一步:ifconfig eno1
lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1 eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.16.30.147 netmask 255.255.255.0 broadcast 172.16.30.255 inet6 fe80::ca7:d954:67e0:7c60 prefixlen 64 scopeid 0x20<link> ether f8:b4:6a:bd:dd:92 txqueuelen 1000 (Ethernet) RX packets 3678600 bytes 3470673356 (3.4 GB) RX errors 0 dropped 36842 overruns 0 frame 0 TX packets 2229431 bytes 995696588 (995.6 MB) TX errors 0
我们经常使用ifconfig查看ip,但是使用ifconfig返回的数据过多,而我们实际使用的部分只是一部分而已。
所以我们使用ifconfig指定设备查找ip,筛去无用信息。
第二步:ifconfig eno1 | grep “inet”
把第一步查询的信息通过 | 产生一个管道传递给下一个命令,用grep查找有inet字符的行数据,显示如下:
lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1 | grep "inet" inet 172.16.30.147 netmask 255.255.255.0 broadcast 172.16.30.255 inet6 fe80::ca7:d954:67e0:7c60 prefixlen 64 scopeid 0x20<link>
因为我们只需要ipv4协议的ip,所以我们要去掉inet6对应的地址
第三步:ifconfig eno1 | grep “inet” | grep -v inet6
使用grep -v命令去掉 inet6 关键词的对应一行信息
lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1 | grep "inet" | grep -v inet6 inet 172.16.30.147 netmask 255.255.255.0 broadcast 172.16.30.255
第四步:ifconfig eno1 | grep “inet” | grep -v inet6 | awk ‘{ print$2}’
使用 awk处理文本文件的语言进行处理数据,$2 表示默认以空格分割的第二组 ,-F:-F指定分隔符为 ‘ : ’
lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1 | grep "inet" | grep -v inet6| awk '{ print$2}' 172.16.30.147
关于grep sed awk的使用大家也可以网上具体查看一下,但是我们一般使用过程中,grep 更适合单纯的查找或匹配文本, sed 更适合编辑匹配到的文本,awk 更适合格式化文本,对文本进行较复杂格式处理。
这个时候我们从本机得到了ip地址。我们需要进行远程调用服务器脚本,并把ip以参数形式传入。
其次ssh免密登陆和ssh远程执行任务
首先第一个部分就是ssh免密登陆
本地执行ssh到服务的相关操作命令需要免密,服务器scp本地文件也要免密登陆,那么如设置我们ssh相关命令操作,无需密码呢?
SSH分客户端openssh-client和服务器openssh-server
如果你只是想登陆别的机器,只需要安装openssh-client(ubuntu有默认安装,如果没有则sudo apt-get install openssh-client),如果要使别的机器登陆本机就需要在本机安装openssh-server(sudo apt-get install openssh-server)
我们可以使用 ps -e | grep ssh 来查看对应的openssh-client和openssh-server运行情况,其中ssh是client ,sshd是server,哪个缺我们就使用apt-get install 。
sudo service ssh start 安装之后可以使用这个命令启动。
准备好了对应的server和client接下来,把我们生成的rsa公钥拷贝要对应要登陆的机器,即可免密登陆。
1.客户端生成公私钥
ssh-keygen 命令一路回车默认生成
这个命令会在用户目录.ssh文件夹下创建公私钥,id_rsa (私钥),id_rsa.pub (公钥)。
2.上传公钥到服务器
ssh-copy-id -i ~/.ssh/id_rsa.pub lyn@172.160.111.32
上面这条命令是写到服务器上的ssh目录下去了
vi ~/.ssh/authorized_keys
可以看到客户端写入到服务器的 id_rsa.pub (公钥)内容。
3.测试免密登录
客户端通过ssh连接远程服务器,就可以免密登录了。
ssh lyn@172.160.111.32
第二个部分就是ssh远程执行服务器脚本
有时候我们需要远程执行一些有交互操作的命令。这个时候我们就可以使用ssh加参数进去进行远程执行。
格式如下:
远程执行一个命令
ssh lyn@172.160.111.32 "ls -l"
执行多条命令,使用分号把不同的命令隔起来
ssh lyn@172.160.111.32 "ls;cat test.txt "
远程执行本地脚本
ssh lyn@172.160.111.32 < test.sh
远程执行本地的脚本(执行带有参数的脚本),需要为 bash
指定 -s 参数:
ssh lyn@172.160.111.32 'bash -s' < test.sh helloworld
执行远程的脚本
ssh lyn@172.160.111.32 "/home/lyn/test.sh"
注,此时需要指定脚本的绝对路径!
而我们使用的为远程执行脚本,最终ssh远程执行如下:
remote_ip=172.160.111.32 remote_hostname=lyn local_ip=172.16.30.147 build_opt= ssh -t ${remote_hostname}@${remote_ip} "/home/lyn/build.sh ip=${local_ip} ${build_opt}"
脚本二:服务器编译脚本 build.sh
脚本内容如下:
#!/bin/bash -e scp_dir=/media/lyn/win_data/lyn_workdata/working/robot-ctl download_data=/home/lyn/Documents/work-data/download_data build_dir=/home/lyn/projects/yocto/yocto-build/tmp/work/aarch64-poky-linux/robot-ctl/git-r0/git/ image_dir=/home/lyn/projects/yocto/yocto-build/tmp/work/aarch64-poky-linux/robot-ctl/git-r0/image/robot-ctl/ remote_exec_file_dir=/home/lyn/Documents/work-data/download_data/scp_exec.sh all_build=No wifi_src=No only_scp_robot=No strip_mode=No ip_wireless_dhcp=170.160.111.45 ip_ethernet_dhcp=170.160.111.147 local_ip=${ip_wireless_dhcp} host_name=lyn date echo -e "\033[0;31m Loading options.\033[0m" # Load all the options. if [ $# -eq 0 ];then echo -e "\033[33;5m no argument \033[0m" fi for arg in "${@}" do if [[ -n "${arg}" ]] && [[ "${arg}" == "wifi" ]] ; then wifi_src="Yes" local_ip=${ip_ethernet_dhcp} echo -e "local connect robot wifi \n --${wifi_src}\n --ip:${local_ip}." fi if [[ -n "${arg}" ]] && [[ "${arg}" == "scp" ]] ; then only_scp_robot="Yes" local_ip=${ip_ethernet_dhcp} echo -e "scp robot local connect robot wifi \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}." fi if [[ -n "${arg}" ]] && [[ "${arg}" == "all_build" ]] ; then all_build="Yes" echo -e "all bulid" fi if [[ -n "${arg}" ]] && [[ "${arg}" == "ip" ]] ; then echo -e "\033[32m ip:=\033[0m" read ip_addr local_ip=${ip_addr} wifi_src="Yes" echo -e "scp robot local ip insert \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}." fi if [[ -n "${arg}" ]] && [[ "${arg:0:3}" == "ip=" ]] ; then echo -e "\033[32m ip put:=\033[0m" ${arg#*=} local_ip=${arg#*=} wifi_src="Yes" echo -e "scp robot local ip insert \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}." fi if [[ -n "${arg}" ]] && [[ "${arg}" == "debug" ]] ; then echo -e "\033[32m need \033[0m" ${arg} strip_mode="Yes" echo -e "scp robot local ip insert \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}." fi done if [ "${only_scp_robot}" == "No" ];then if [ ! "${wifi_src}" == "Yes" ] ; then echo -e "local don't connect robot wifi \n --${wifi_src}\n --ip:${local_ip}." fi if [ -d "${build_dir}" ]; then cd ${build_dir} scp -rp lyn@{local_ip}:${scp_dir}/src \ lyn@{local_ip}:${scp_dir}/include \ lyn@{local_ip}:${scp_dir}/CMakeLists.txt . //ssh ${host_name}@${local_ip} "${remote_scp_code_dir}" #git pull if [ "${all_build}" == "Yes" ] ; then ./../temp/run.do_generate_toolchain_file ./../temp/run.do_configure fi ./../temp/run.do_compile ./../temp/run.do_install #cd /home/lyn/ if [ "${strip_mode}" == "No" ]; then #./strip_x1000.sh cd ${image_dir} aarch64-linux-gnu-strip pp fi else echo -e "\033[0;31m dir is not exist.\033[0m" fi fi scp ${image_dir}/exec ${host_name}@${local_ip}:${download_data} if [[ "${wifi_src}" == "Yes" ]] || [[ "${only_scp_robot}" ]] ; then ssh ${host_name}@${local_ip} "${remote_exec_file_dir}" else echo -e "no robot wifi\n" fi
服务器执行的脚本内容比较长,从执行的流程来说,在这个脚本中,大致为初始化读取脚本执行传入的参数,通过参数配置不同的变量匹配不同机器状态,紧接着,拷贝本地的文件到服务器编译,服务器编译完成之后拷贝可执行文件到本地,再调用本地的脚本把可执行文件拷贝到机器对应的目录。
这个脚本有些使用技术和第一个脚本有重合,此处仅说没有讲到的部分。
首先第一个使用的就是 { KaTeX parse error: Expected 'EOF', got '#' at position 1: #̲ } 和 {@ }
下面是从菜鸟教程拷贝的shell传参的特殊字符介绍
参数处理 | 说明 |
$# | 传递到脚本的参数个数 |
$* | 以一个单字符串显示所有向脚本传递的参数。 如"$*“用「”」括起来的情况、以"$1 $2 … $n"的形式输出所有参数。 |
$$ | 脚本运行的当前进程ID号 |
$! | 后台运行的最后一个进程的ID号 |
$@ |
与∗ 相 同 , 但 是 使 用 时 加 引 号 , 并 在 引 号 中 返 回 每 个 参 数 。 如 " *相同,但是使用时加引号,并在引号中返回每个参数。 如"∗相同,但是使用时加引号,并在引号中返回每个参数。如"@“用「”」括起来的情况、以"$1" “2 " … " 2" … "2"…"n” 的形式输出所有参数。 |
$- | 显示Shell使用的当前选项 |
$? | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。 |
我在其中使用了$#用来辅助提醒输入的参数数量,防止出错;
if [ $# -eq 0 ];then echo -e "\033[33;5m no argument \033[0m" fi
然后使用$@把传入的参数一个个解析出了来,进行变量的配置。
for arg in "${@}" do if [[ -n "${arg}" ]] && [[ "${arg}" == "wifi" ]] ; then wifi_src="Yes" local_ip=${ip_ethernet_dhcp} echo -e "local connect robot wifi \n --${wifi_src}\n --ip:${local_ip}." fi ... done
其次使用了字符串截取的操作
if [[ -n "${arg}" ]] && [[ "${arg:0:3}" == "ip=" ]] ; then echo -e "\033[32m ip put:=\033[0m" ${arg#*=} local_ip=${arg#*=} wifi_src="Yes" echo -e "scp robot local ip insert \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}." fi
${arg:0:3} 意思为从左边第0个字符开始,字符的个数为3个
${arg#*=} 意思为 # 号截取,删除 '='左边字符,保留右边字符。
至于为什么这么使用,以及其他使用的介绍,这里我摘录其他博主的文章给大家做一个简单的分享
摘录自:《shell脚本字符串截取的8种方法》
假设有变量 var=http://www.aaa.com/123.htm.
1. # 号截取,删除左边字符,保留右边字符。
echo ${var#*//}
其中 var 是变量名,# 号是运算符,*// 表示从左边开始删除第一个 // 号及左边的所有字符
即删除 http://
结果是 :www.aaa.com/123.htm
2. ## 号截取,删除左边字符,保留右边字符。
echo ${var##*/}
##*/ 表示从左边开始删除最后(最右边)一个 / 号及左边的所有字符
结果是 123.htm
3. %号截取,删除右边字符,保留左边字符
echo ${var%/*}
%/* 表示从右边开始,删除第一个 / 号及右边的字符
4. %% 号截取,删除右边字符,保留左边字符
echo ${var%%/*}
%%/* 表示从右边开始,删除最后(最左边)一个 / 号及右边的字符
结果是:http:
**5. 从左边第几个字符开始,及字符的个数 **
echo ${var:0:5}
其中的 0 表示左边第一个字符开始,5 表示字符的总个数。
结果是:http:
6. 从左边第几个字符开始,一直到结束。
echo ${var:7}
其中的 7 表示左边第8个字符开始,一直到结束。
结果是 :www.aaa.com/123.htm
7. 从右边第几个字符开始,及字符的个数
echo ${var:0-7:3}
其中的 0-7 表示右边算起第七个字符开始,3 表示字符的个数。
结果是:123
8. 从右边第几个字符开始,一直到结束。
echo ${var:0-7}
表示从右边第七个字符开始,一直到结束。
结果是:123.htm
注:(左边的第一个字符是用 0 表示,右边的第一个字符用 0-1 表示)
此外我们也可以使用awk、cut进行截取,这里就不一一列举了。
脚本三:本地expect登陆拷贝scp_exec.sh脚本
脚本内容如下:
#!/bin/expect set timeout 30 set host 192.168.1.1 set user root spawn scp /home/lyn/Documents/work-data/download_data/ $user@$host:/opt/lib/exec #spawn ssh $user@$host expect { "*yes/no*" { send "yes\r" expect "*password:*" { send "123456\r" } } "*password:*" { send "123456\r" } } expect eof
因为服务器地址相对固定,并且方便设置ssh公钥免密登陆,但是机器而言,你需要调试的机器有很多,我就没有考虑使用了expect命令。先给大家简单介绍一下expect:
Expect是等待输出内容中的特定字符。然后由send发送特定的相应。其交互流程是:
spawn启动指定进程 -> expect获取指定关键字 -> send想指定进程发送指定指令 -> 执行完成, 退出.
首先使用expect 我们需要安装expect
sudo apt-get install tcl tk expect
因为我写的这个部分也比较简单,所以就一点点给大家说明里面执行细节:
#!/bin/expect
expect的目录,类似与shell目录
set timeout 30
set 自定义变量名”:设置超时时间,单位为秒,有些拷贝大文件的朋友可能会遇到
expect: spawn id exp4 not open这里没有等到expect eof是,ssh连接已经关闭了。一般是超时时间太短了,
建议可以直接设置成 timeout -1,这意味着用不超时,拷贝结束之后才会断开。
set host 192.168.1.1 set user root spawn scp /home/lyn/Documents/work-data/download_data/ $user@$host:/opt/lib/exec
spawn(expect安装后的命令)是进入expect环境后才可以执行的expect内部命令,它主要的功能是给ssh运行进程加个壳,用来传递交互指令。可以理解为启动一个新进程 。
**`expect { "*yes/no*" { send "yes\r" expect "*password:*" { send "123456\r" } } "*password:*" { send "123456\r" } }`**
expect {}: 多行期望,从上往下匹配,匹配成功里面的哪一条,将执行与之的
send命令,注意,这里面的匹配字符串只会执行一个,即匹配到的那个,其余
的将不会执行,如果想匹配这句命令执行成功后(如登录成功后等待输入的root
@ubuntu:~#)的其他字符,需要另起一个expect命令,并保证不在expect{}里
面。
`send "yes\r"
send接收一个字符串参数,并将该参数发送到进程。这里就是执行交互动作,
与手工输入密码的动作等效。 命令字符串结尾别忘记加上“\r”,表示“回车
键”。
expect eof
expect执行结束, 退出交互程序。
这里我只是简单描述了一下我使用expect文件,更多expect命令学习,有兴趣的朋友,可以自行搜索学习。
结语
这就是我分享我在工作中使用的shell脚本,如果大家有更好的想法和需求,也欢迎大家加我好友交流分享哈。
作者:良知犹存,白天努力工作,晚上原创公号号主。公众号内容除了技术还有些人生感悟,一个认真输出内容的职场老司机,也是一个技术之外丰富生活的人,摄影、音乐 and 篮球。关注我,与我一起同行。