2022-06-27: Password Generator Program in Scheme

A simple command-line program using the generate-password procedure from (robin text) of r7rs-libs.

The password-generator procedure accepts five arguments, some flags telling it which characters to choose from, and a size, specifying the length of the password. Two values are returned, a generated password and its entropy. In some cases, an error is raised.

The aim here is to provide the various arguments from the command-line, call the procedure, and display its return values. Any errors should be handled smoothly, with appropriate help messages.

Processing Command-Line Arguments

There are various ways of handling the command-line arguments, and at least two possible libraries: slib's getopt and srfi 37. For this program, I decided to go with a hand-rolled solution.

The program's command-line arguments can be retrieved using the command-line procedure, available in (scheme process-context). This returns a list of the arguments, the first of which is the program's name. We will handle two cases:

The "flags" is a list of single letter options, representing the five flag-arguments to the generate-password procedure. We will extract these two cases in a case statement:

(let ((arguments (command-line)))                               ; <1>
  (case (length arguments)                                      ; <2>
    ((2)                                                        ; <3>
     (generate-password-with-args (list-ref arguments 1)
                                  ""))
    ((3)                                                        ; <4>
     (generate-password-with-args (list-ref arguments 2)
                                  (list-ref arguments 1)))
    (else                                                       ; <5>
      (help-message))))
  1. Retrieve the arguments using command-line.
  2. Look for the appropriate number of arguments:
  3. Two arguments means we have the "size" parameter only, so call the next procedure with a default, empty flags value.
  4. Three arguments means we have the "size" and some "flags", so pass them both to the next procedure.
  5. Anything else is an error, so print a helpful message.

The "next procedure" must parse the flags and convert "size" to a number.

For "flags", we simply convert all the flags to lower case and split them into a list of characters - this means we can check if a given character is a member of the list, and so whether that character's flag has been selected.

Note that no flags means that all options should be set to #t, hence each flag is set as (or (null? flags) (member flag-char flags)):

(define (generate-password-with-args size-arg flags-arg)
  (let* ((size (string->number size-arg))                       ; <1>
         (flags (string->list (string-downcase flags-arg)))     ; <2>
         (allow-duplicates? (or (null? flags)                   ; <3>
                                (member #\d flags)))            ; <4>
         (use-lowercase? (or (null? flags)
                             (member #\l flags)))
         (use-numbers? (or (null? flags)
                           (member #\n flags)))
         (use-symbols? (or (null? flags)
                           (member #\s flags)))
         (use-uppercase? (or (null? flags)
                             (member #\u flags))))
    ; call (generate-password size 
    ;                         allow-duplicates? 
    ;                         use-lowercase? 
    ;                         use-numbers? 
    ;                         use-symbols? 
    ;                         use-uppercase?)
    ))
  1. Convert size to a number - this will be #f if not a number. We should check for an error here, but generate-password checks for us.
  2. Convert the flags string into a list of lower case characters.
  3. A flag is true if there are no flags, or ...
  4. ... the flag's character is contained in the list of provided flag characters.

If we uncomment the call above, and put in some code to collect and print the return values, we will have a working program. However, any errors will look unpleasant to the user.

Error Handling

Error handling in R7RS is managed with guard. Guard catches any raised error or condition, and provides a cond-style matching system to check for the type of error condition and provide a suitable response. In this case, we only have errors to handle:

    (guard (condition                                                     ; <1>
             ((error-object? condition)                                   ; <2>
              (show #t "Error: " (error-object-message condition) nl)     ; <3>
              (help-message)))
      (let-values (((password entropy) (generate-password size 
                                                          allow-duplicates? 
                                                          use-lowercase? 
                                                          use-numbers? 
                                                          use-symbols? 
                                                          use-uppercase?)))
        (show #t "Password is: " nl password nl "with entropy: " entropy nl)))
  1. Catch any raised error in the condition variable.
  2. Check that we have an error object.
  3. Print a suitable message, including the error message itself.

Final Program

(import (scheme base)
        (scheme char)
        (scheme inexact)
        (scheme process-context)
        (scheme show)                                                     ; <1>
        (robin text))

(define (help-message)
  (show #t "password-generator: [flags] size" nl) 
  (show #t "  d  allow-duplicates" nl) 
  (show #t "  l  use lowercase letters" nl) 
  (show #t "  n  use numbers" nl) 
  (show #t "  s  use symbols" nl) 
  (show #t "  u  use uppercase letters" nl) 
  (show #t nl "Not specifying any flags will use all groups with duplicates." nl))

(define (generate-password-with-args size-arg flags-arg)                  ; <2>
  (let* ((size (string->number size-arg))
         (flags (string->list (string-downcase flags-arg)))               ; <3>
         (allow-duplicates? (or (null? flags)                             ; <4>
                                (member #\d flags)))
         (use-lowercase? (or (null? flags)
                             (member #\l flags)))
         (use-numbers? (or (null? flags)
                           (member #\n flags)))
         (use-symbols? (or (null? flags)
                           (member #\s flags)))
         (use-uppercase? (or (null? flags)
                             (member #\u flags))))
    ; 
    (guard (condition                                                     ; <5>
             ((error-object? condition)                                   ; <6>
              (show #t "Error: " (error-object-message condition) nl)     ; <7>
              (help-message)))
      (let-values (((password entropy) (generate-password size            ; <8>
                                                          allow-duplicates? 
                                                          use-lowercase? 
                                                          use-numbers? 
                                                          use-symbols? 
                                                          use-uppercase?)))
        (show #t "Password is: " nl password nl "with entropy: " entropy nl)))))

;; get options from command line
;; - [flags] number
(let ((arguments (command-line)))                                         ; <9>
  (case (length arguments)
    ((2)                                                                  ; <10>
     (generate-password-with-args (list-ref arguments 1)
                                  ""))
    ((3)
     (generate-password-with-args (list-ref arguments 2)
                                  (list-ref arguments 1)))
    (else
      (help-message))))
  1. Or (srfi 159)
  2. Procedure to process the command-line flags, get and display the password.
  3. Convert flags into a list of characters, so ...
  4. ... flag values can be found if the required character is present. Notice that flag values are true by default, unless some flags are set.
  5. The called procedure may raise an error, so we will guard against that.
  6. Any error condition is recognised ...
  7. ... and its message displayed, along with the program help.
  8. Otherwise, the called procedure should return its two values for display.
  9. Top-level code to retrieve the command line arguments ...
  10. ... and process them.

Install

On windows (linux/mac are not too different), create a folder which you add to your PATH environment variable - I have a folder called "~/bin".

Copy the above program into this folder, and create a file "password-generator.bat" with contents:

@echo off
gosh "%~dp0password-generator.sps" %*

Modify the above for your preferred flavour of Scheme. I'm assuming the `(robin text)` library from r7rs-libs is on your Scheme's loadpath.

You can now directly run the password-generator program within your shell.

Examples

Simple call, returns a password of required length.

> password-generator 10
Password is:
N!cPxg!i>)
with entropy: 64.91853096329675

A longer password has increased entropy:

> password-generator 12
Password is:
bOu8-mJl~AQo
with entropy: 77.9022371559561

The possible set of characters to draw from can be specified in the flags:

> password-generator ul 12
Password is:
jVnlLOGcJpwU
with entropy: 68.40527661769312

Some errors are recognised, such as if the permitted set of characters is too small to generate the required size of password.

> password-generator ul 120
Error: No duplicates is selected, but the required password size exceeds the number of available characters.
password-generator: [flags] size
  d  allow-duplicates
  l  use lowercase letters
  n  use numbers
  s  use symbols
  u  use uppercase letters

Not specifying any flags will use all groups with duplicates.

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.