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.
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:
- program-name size
- program-name flags size
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))))
-
Retrieve the arguments using
command-line
. - Look for the appropriate number of arguments:
- Two arguments means we have the "size" parameter only, so call the next procedure with a default, empty flags value.
- Three arguments means we have the "size" and some "flags", so pass them both to the next procedure.
- 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?) ))
-
Convert size to a number - this will be
#f
if not a number. We should check for an error here, butgenerate-password
checks for us. - Convert the flags string into a list of lower case characters.
- A flag is true if there are no flags, or ...
- ... 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 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)))
-
Catch any raised error in the
condition
variable. - Check that we have an error object.
- Print a suitable message, including the error message itself.
(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))))
-
Or
(srfi 159)
- Procedure to process the command-line flags, get and display the password.
- Convert flags into a list of characters, so ...
- ... flag values can be found if the required character is present. Notice that flag values are true by default, unless some flags are set.
- The called procedure may raise an error, so we will guard against that.
- Any error condition is recognised ...
- ... and its message displayed, along with the program help.
- Otherwise, the called procedure should return its two values for display.
- Top-level code to retrieve the command line arguments ...
- ... and process them.
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.
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.