メモの日々(2009-05-11)

メモの日々


2009年05月11日(月) [長年日記]

  • 連休が明けたばかりだからか夜眠れない。朝になってやっと眠くなる。
  • 夏のように暑い。

[ruby] Net::SSH でコマンドの終了コードを得る

(追記:Net::SSHの最新のドキュメントはgithub上のものみたい。)

Net::SSHNet::SSH::Connection::Session#exec()で簡単にリモートでのコマンド実行ができる。が、このメソッドではコマンドの終了コードを得られず困る。

Net::SSH::Connection::Channel#on_request()を使うことで終了コードを取得できたので、サンプルスクリプトと実行結果をメモ。

require "rubygems"
require "net/ssh"

include Net::SSH::Prompt

def parse_args
  if ARGV.size < 2 || ARGV.size > 3
    $stderr.puts "Usage: #{$0} HOST USER [PASSWORD]"
    exit 2
  end
  host = ARGV[0]
  user = ARGV[1]
  password = ARGV[2] || prompt("Enter password for #{user}@#{host}:", false)
  return host, user, password
end

# ssh session の close を保証する
def ssh_start(host, user, password)
  begin
    session = Net::SSH.start(host, user, :password => password)
    yield session
  rescue => ex
    puts "Exception: #{ex.message}"
    ex.backtrace.each { |trace| puts trace }
  ensure
    session.close if session
  end
end

def ssh_exec(session, command)
  exit_status = nil
  session.open_channel { |channel|
    # Channel#on_request() を使うと終了コードを取得できる
    channel.on_request("exit-status") { |ch, data|
      exit_status = data.read_long
    }
    # 標準出力は Channel#on_data() で扱う
    channel.on_data { |ch, data|
      data.each_line { |line| puts "[remote:out] #{line}" }
    }
    # 標準エラーは Channel#on_extended_data() で扱う
    channel.on_extended_data { |ch, type, data|
      data.each_line { |line| puts "[remote:err] #{line}" if type == 1 }
    }
    channel.exec(command)
  }
  session.loop
  return exit_status
end

ssh_start(*parse_args) { |session|
  puts "Exit status: #{ssh_exec(session, "ps | grep ssh")}"
  puts "Exit status: #{ssh_exec(session, "sp")}"
  puts "Exit status: #{ssh_exec(session, "cat nantoka")}"
}

実行結果は次の通り。

$ ruby sshexec.rb localhost kenichi
Enter password for kenichi@localhost:
[remote:out]  3668 ?        00:00:02 ssh-agent
[remote:out] 10067 ?        00:00:00 sshd
[remote:out] 11922 ?        00:00:00 sshd
Exit status: 0
[remote:err] zsh:1: command not found: sp
Exit status: 127
[remote:err] cat:
[remote:err] nantoka
[remote:err] : No such file or directory
[remote:err]
Exit status: 1

[shell][unix] /usr/bin/getopt を使ったサンプル

/usr/bin/getoptを使うとシェルスクリプトの引数の解析が簡単(でもないけど)にできるが、使い方を覚えられないのでメモ。

  • getoptは与えられたパラメータを並び変えた結果を出力する。また、パラメータのエラーを検出してくれる。
  • ロングオプションも扱える。
  • getoptが並び変えた結果を自前で解析する必要がある。
  • getoptが行うクォート処理を正しく扱う必要がある。ここ理解しきれていなくて、spikelet daysにある例の真似をした。
  • 少なくともbashでは「set --」を使って位置パラメータを置き換えることができる。

以下サンプルスクリプトと実行結果。

#!/bin/bash

# -a     引数必須
# -b     引数なし
# --long 引数必須
# $@を""で囲む必要がある。
args=`getopt -o a:b -l long: -- "$@"`

# getoptの終了コードで書式に間違いがあるかが分かる
if [ "$?" -ne 0 ]; then
    echo "usage: $0 [-a VALUE] [-b] [--long VALUE] ARGS" >&2
    exit 2
fi

# getoptはパラメータの順序を整理した結果を出力する
echo 'getopt' output: $args

# 位置パラメータ($1, $2, ...)の内容を再設定する
# ""で囲った結果をevalで実行する必要がある
eval set -- "$args"

until [ "$1" == "--" ]; do
    case $1 in
    -a)
        a=$2
        shift
        ;;
    -b)
        b=true
        ;;
    --long)
        long=$2
        shift
        ;;
    esac
    shift
done
shift # '--' を取り除く

echo a=$a
echo b=$b
echo long=$long
for arg; do echo arg=$arg; done

実行結果は次の通り。

$ type getopt
getopt is /usr/bin/getopt

$ getopt --version
getopt (enhanced) 1.1.4

$ ./getopt.sh abc def
getopt output: -- 'abc' 'def'
a=
b=
long=
arg=abc
arg=def

$ ./getopt.sh 'abc def'
getopt output: -- 'abc def'
a=
b=
long=
arg=abc def

$ ./getopt.sh abc def --oreore
getopt: unrecognized option '--oreore'
usage: ./getopt.sh [-a VALUE] [-b] [--long VALUE] ARGS

$ ./getopt.sh abc def --long
getopt: option '--long' requires an argument
usage: ./getopt.sh [-a VALUE] [-b] [--long VALUE] ARGS

$ ./getopt.sh abc def --long=loooong -a 's p a c e' -b
getopt output: --long 'loooong' -a 's p a c e' -b -- 'abc' 'def'
a=s p a c e
b=true
long=loooong
arg=abc
arg=def