“@server1 @server2 restart tomcat” --- 以Twitter(微博)的语法风格执行ssh、scp命令

平常工做中,若是常常须要从工做机登陆到其余机器或服务器,在其余机器批量执行命令,或使用scp批量上传/下载文件的话,每每须要敲屡次重复冗长的ssh命令。例如:shell

# 登陆某台机器
ssh deploy@192.168.1.200
ssh tomcat@192.169.2.150

# 批量执行某个命令
ssh admin@192.168.1.200 hbase-daemon.sh restart regionserver
ssh admin@192.168.1.201 hbase-daemon.sh restart regionserver
ssh admin@192.168.1.202 hbase-daemon.sh restart regionserver

# 上传某个文件夹
scp -r classes tomcat@192.168.1.150:/home/tomcat_base/WEB-INF/
scp -r classes tomcat@192.168.1.151:/home/tomcat_base/WEB-INF/

# 下载某个文件夹
scp -r admin@192.168.1.200:/user/local/hbase/conf ./

其实,在目标机器的地址和用户名均已知的状况下,若能经过给它们预设一些别名,并采用Twitter(微博)的“圈人”方式执行ssh或scp命令,则会十分的快速方便。即:tomcat

# 登陆某台机器。直接@该机器的别名
@200
@tomcat1

# 批量执行某个命令。@各机器的别名并跟上要执行的命令
@hbase1 @hbase2 @hbase3 hbase-daemon.sh restart regionserver

# 上传某个文件夹。@各机器的别名,加上u表示上传(Upload),再跟上本地路径和远程路径
@tomcat1 @tomcat2 u classes /home/tomcat_base/WEB-INF/

# 下载某个文件夹。@机器的别名,加上d表示下载(Download),再跟上远程路径和本地路径
@hbase1 d /user/local/hbase/conf ./

如何实现以上的语法?要知道尝试在shell执行以@开头的命令确定会报错,例如:bash

$ @123
-bash: @123: command not found

但Bash默认使用一个函数command_not_found_handle来处理找不到的命令并对用户进行提示。咱们只要修改这个函数,就能让shell在遇到以@开头的命令时,执行ssh或scp命令。服务器

以Ubuntu 14.04为例,报错函数位于/etc/bash.bashrc,定义以下:ssh

        function command_not_found_handle {
                # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
                   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
                   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
                else
                   printf "%s: command not found\n" "$1" >&2
                   return 127
                fi
        }

能够看到该函数在找不到命令时会去某些位置寻找合适的错误处理程序,找到的话就进行调用,不然就简单地报错。函数

在该函数开头拦截以@开头的命令,并调用ssh命令便可实现咱们想要的逻辑:spa

        function command_not_found_handle {

                # 插入代码
                # 若是命令以@开头
                if [[ $1 =~ ^@.* ]]; then
                   # 调用咱们的ssh脚本at.sh
                   at.sh "$@"
                   # 返回
                   return 0
                fi
                # 结束插入代码

                # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
                   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
                   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
                else
                   printf "%s: command not found\n" "$1" >&2
                   return 127
                fi
        }

接下来只要实现咱们的ssh脚本at.sh便可。个人实现以下:命令行

# 配置各机器的别名。每行一台机器,语法为“别名1|别名2|别名3...=用户名@地址”,当机器存在多个别名时,能够经过@任意一个别名访问该机器
presets="
200|hbase1=service@192.168.1.200
201|hbase2=service@192.168.1.201
202|hbase3=service@192.168.1.202
150|tomcat1=service@192.168.2.150
151|tomcat2=service@192.168.2.151
"
# 存储用户@到的别名列表
targets=()
# 是否执行scp(false视为执行ssh)
scp=false
# 存储ssh待执行的命令
ssh_cmd=""
# 当执行scp时,是不是要下载文件(false视为上传)
scp_down=false
# 执行scp时的源文件路径
scp_a=""
# 执行scp时的目标文件路径
scp_b=""
# 根据用户@到的别名查找机器,返回“用户名@地址”形式的机器地址,找不到则返回空字符串
function find_preset() {
  IFS=$'\n'
  for preset_line in $presets; do
    IFS='=' read -a splitted_preset_line <<< "$preset_line"
    preset_aliases="${splitted_preset_line[0]}"
    preset="${splitted_preset_line[1]}"
    if [[ "|${preset_aliases}|" = *\|$1\|* ]]; then
      echo "$preset"
      return
    fi
  done
}
# 遍历命令行参数
for ((i=1;i<=$#;i++)); do
  # 若是不以@开头
  if ! [[ ${!i} =~ ^@.* ]]; then
    # 若是别名列表后遇到“u”或者“d”,意味着用户要使用scp
    if [[ ${!i} = "u" ]] || [[ ${!i} = "d" ]]; then
      # 设scp标识
      scp=true
      # 若是是下载
      if [[ ${!i} = "d" ]]; then
        # 设下载标识
        scp_down=true
      fi
      # 把后面两个参数存到scp的源文件路径和目标文件路径中去
      i=$(expr $i + 1)
      scp_a="${!i}"
      i=$(expr $i + 1)
      scp_b="${!i}"
    # 不然是ssh调用
    else
      # 把后续的全部参数拼接并存储到ssh命令变量中
      ssh_cmd="${!i}"
      for ((j=i+1;j<=$#;j++)); do
        ssh_cmd="$ssh_cmd ${!j}"
      done
    fi
    break
  fi
  # 去掉参数开头的“@”
  target_with_at="${!i}"
  # 存储到目标别名列表
  targets[i]="${target_with_at:1}"
done
# 遍历全部目标别名
for target in ${targets[*]}; do
  # 根据目标别名查找机器
  preset=$(find_preset $target)
  # 找不到则报错并跳过该别名
  if ! [ -n "$preset" ]; then
    echo -e "\e[31mUnknown target: $target \e[0m"
    continue
  fi
  # 若是是scp调用
  if $scp; then
    @ 若是是下载
    if $scp_down; then
      # 执行scp下载
      scp -r "$preset:$scp_a" "$scp_b"
    # 不然是上传
    else
      # 执行scp上传
      scp -r "$scp_a" "$preset:$scp_b"
    fi
  # 不然是ssh调用
  else
    # 提示用户正在链接
    echo -e "\e[32mRequesting $preset... \e[0m"
    # 执行ssh命令
    ssh "$preset" "$ssh_cmd"
    # 若是待执行命令为空,意味着用户是要直接登陆该机器,脚本直接退出
    if ! [ -n "$ssh_cmd" ]; then
      break
    fi
  fi
done

最后只要把at.sh放到PATH中去便可。rest

相关文章
相关标签/搜索