PIB - Ruby - password mask

参考になるページ等

memo

Ruby 1.8 世代の古いやり方だと gets 前後で Kernel.#system から stty (1) を呼び -echo と echo で囲むというやり方が行われており net/imap の中でも使われていた(※1)。
gem が使える場合には highline って方法もあるようだが、gem を入れるのにひと手間必要(※2)。
標準添付ライブラリの readline を使う方法もあるようだが、自前でビルドした際にはしばしば GNU readline を入れ忘れている場合がある(※3)。

2016-02-03 現在だと、
Ruby 1.9.3 preview1 以降で追加された標準添付ライブラリ io/console(code) が提供している IO#noecho を使うのが多分一番手軽で標準的な方法のようだ(※2, ※3)。
以下の様な感じ
require 'io/console'

print "Password: "
password = STDIN.noecho(&:gets).chomp!
puts

ただし、highline だとリアルタイムに入力を * で隠すとか出来るみたいなんだけど、
上記 IO#noecho の例だと入力は全く見えない。
これは OpenSSH なんかも、IO#noecho と同じように何も表示されないので、まぁ仕方ないかって感じ。

標準添付ライブラリ io/console が提供している IO#getch を使うと1文字毎にインタラクト出来るので * でマスクとかも出来なくはないんだけど、F1 で \e[OP とか、DEL で \e[3~ とかの可変長エスケープシーケンスが1文字ずつ返ってくる上、Ctrl-c とかも食えちゃうのであまり楽な処理が出来ない。
例えばエスケープシーケンス無視してもこんな感じになる。Gem にするならともかく、これをソースに埋め込むのは実に野暮ったい。

GitHub / kou1okada / getpw.rb

#!/usr/bin/env ruby

require 'io/console'

def getpw
  pw = c = ""
  begin
    c = STDIN.getch
    case c
    when /[\x03]/ # C-c
      Process.kill('SIGINT', Process.pid)
    when /[\x1a]/ # C-z
      Process.kill('SIGSTOP', Process.pid)
    when /[\b\x7f]/ # C-h|BS
      if 0 < pw.length
        print "\b \b"
        pw.chop!
      end
    when /[\x15]/ # C-u
      print "\b \b" * pw.length
      pw = ""
    when /[[:print:]]/
      print "*"
      pw += c
    else
      print "*"
      pw += c
    end
  end until c =~ /[\r\n]/
  pw
end

if __FILE__ == $0
  print "Password: "
  password = getpw.chomp!
  puts
  
  p password
end

関連