terminal:password:get

GPS - Thu May 9, 2002: I use the following code in my file server to receive a password. It is quite nice, because it displays * for each entered character. It supports backspace, and when a control key is pressed it's ignored, instead of appending to the string. It uses the Unix Terminal Extension which is in the public domain. I place this code in the public domain too. Please feel free to extend this.

DL - Sun Oct 10, 2005: I moved the prompt after the terminal mode change. This is necessary to avoid the password getting lost during a mode change.


proc terminal:password:get {str} {
    terminal:canonicalOff
    terminal:echoOff

    puts -nonewline $str
    flush stdout
  
    set chr ""
    set inputStr ""
    while 1 {
      set chr [read stdin 1]
      #Backspace
      if {$chr == "\b"} {
        if {[string length $inputStr] > 0} {
          puts -nonewline "\x1b\[D"
          puts -nonewline " "
          puts -nonewline "\x1b\[1D"
          set lastChar [expr {[string length $inputStr] - 2}]
          set inputStr [string range $inputStr 0 $lastChar]
          flush stdout
        }
        continue
      }
  
      #eat up escape characters
      #example: ESCc ESC\[D ESC\[1D ESC\[11D
      if {$chr == "\x1b"} {
        set nextChar [read stdin 1]
        if {$nextChar == "\["} {
          #This isn't a simple 2 char escape sequence
          #It could be ESC\[D or ESC\[= or ESC\[1D
          set nextChar [read stdin 1]
          if {[string is digit $nextChar] || $nextChar == "="} {
            while 1 {
              #eat up the digits
              set nextChar [read stdin 1]
              if {[string is digit $nextChar]} {
                continue
              } else {
                #We read a char that wasn't a digit, so we are at the end.
                #If the string we had was ESC\[22D we just read D
                break
              }
            }
          }
        }
        continue
      }
  
      if {$chr == "\n" || $chr == "\r"} {
        break
      }
      append inputStr $chr
      puts -nonewline *
      flush stdout
    }
    terminal:canonicalOn
    terminal:echoOn
  
    #DEBUG
    #puts "\n$inputStr"
  
    if {[string length $inputStr] <= 0} {
      return -code error "Please specify one or more characters for your password.\n"
    }
  
    return $inputStr
}

DKF: Here's the cheap-and-cheerful version using stty tricks. :^) It does a bit less, but uses far less code. Unix only.

proc terminal:password:get {promptString} {

     # Turn off echoing, but leave newlines on.  That looks better.
     # Note that the terminal is left in cooked mode, so people can still use backspace
     exec stty -echo echonl <@stdin

     # Print the prompt
     puts -nonewline stdout $promptString
     flush stdout

     # Read that password!  :^)
     gets stdin password

     # Reset the terminal
     exec stty echo -echonl <@stdin

     return $password
}

RJ 03/26/05 Yet another version uses Expect and echoes the "*". Uses raw mode for tty. Again, *NIX only.

proc terminal:password:get {pwprompt}  {

        set oldmode [stty -echo raw]  ;# set to raw and no echo
        send_user "\n     $pwprompt"
        set timeout -1                ;# wait as long as it takes for input - infinite timeout
        set p ""

       # read user input one char at a time
        expect_user {
                -re "\177"      {
                      # handle backspace/delete translation
                        set lastchar [string index $p end]
                        set p [string range $p 0 [expr [string length $p] - 2]]
                        send_user "\010 \010"
                        exp_continue
                        }
                "\r" {}
                      # password entry complete - drop out of expect_user
                "\003" { exit }
                      # cntl-C entered - abort altogether
                -re "." {
                      # character entry - add to pw variable and echo a "*"
                        append p $expect_out(buffer)
                        send_user "*"
                        exp_continue
                        }
                    }
  
        send_user "\n"                       ;# send the user the carriage return eaten above by expect_user
        eval stty $oldmode                   ;# restore echo and cooked mode saved above
        return [string trimright $p "\r"]    ;# strip carriage return and pass back pw
}

DKF See also Echo-free Password Entry, and Reading a single character from the keyboard using Tcl (the techniques used there apply here too, though you need to apply them slightly differently).