Create a password generation program which will generate passwords containing random ASCII characters from the following groups:

         lower-case letters:  a ──► z
         upper-case letters:  A ──► Z
                     digits:  0 ──► 9
 other printable characters:  !"#$%&'()*+,-./:;<=>?@[]^_{|}~ 
 (the above character list excludes white-space, backslash and grave) 



The generated password(s) must include   at least one   (of each of the four groups):

   lower-case letter, 
   upper-case letter,
   digit  (numeral),   and 
   one  "other"  character. 



The user must be able to specify the password length and the number of passwords to generate.

The passwords should be displayed or written to a file, one per line.

The randomness should be from a system source or library.

The program should implement a help option or button which should describe the program and options when invoked.

You may also allow the user to specify a seed value, and give the option of excluding visually similar characters.

For example:            Il1     O0     5S     2Z            where the characters are:

   capital eye, lowercase ell, the digit one
   capital oh, the digit zero 
   the digit five, capital ess
   the digit two, capital zee


with Ada.Numerics.Discrete_Random;
with Ada.Strings.Fixed;
with Ada.Command_Line;
with Ada.Integer_Text_IO;
with Ada.Text_IO;
 
procedure Mkpw is
 
   procedure Put_Usage;
   procedure Parse_Command_Line (Success : out Boolean);
   function Create_Password (Length : in Positive;
                             Safe   : in Boolean)
                            return String;
 
   procedure Put_Usage is
      use Ada.Text_IO;
   begin
      Put_Line ("Usage:                                                     ");
      Put_Line ("   mkpw help   - Show this help text                       ");
      Put_Line ("   mkpw [count <n>] [length <l>] [seed <s>] [safe]         ");
      Put_Line ("     count <n>  - Number of passwords generated (default 4)");
      Put_Line ("     length <l> - Password length (min 4)       (default 4)");
      Put_Line ("     seed <l>   - Seed for random generator     (default 0)");
      Put_Line ("     safe       - Do not use unsafe characters  (0O1lIS52Z)");
   end Put_Usage;
 
   Lower_Set  : constant String := "abcdefghijklmnopqrstuvwxyz";
   Upper_Set  : constant String := "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
   Digit_Set  : constant String := "0123456789";
   Spec_Set   : constant String := "!""#$%&'()*+,-./:;<=>?@[]^_{|}~";
   Unsafe_Set : constant String := "0O1lI5S2Z";
 
   Full_Set : constant String := Lower_Set & Upper_Set & Digit_Set & Spec_Set;
   subtype Full_Indices is Positive range Full_Set'Range;
 
   package Full_Index_Generator is
      new Ada.Numerics.Discrete_Random (Full_Indices);
 
   Index_Generator : Full_Index_Generator.Generator;
 
   Config_Safe          : Boolean  := False;
   Config_Count         : Natural  := 4;
   Config_Length        : Positive := 6;
   Config_Seed          : Integer  := 0;
   Config_Show_Help     : Boolean  := False;
 
   procedure Parse_Command_Line (Success : out Boolean) is
      use Ada.Command_Line;
      Got_Safe,   Got_Count : Boolean  := False;
      Got_Length, Got_Seed  : Boolean  := False;
      Last       : Positive;
      Index      : Positive := 1;
   begin
      Success := False;
      if Argument_Count = 1 and then Argument (1) = "help" then
         Config_Show_Help := True;
         return;
      end if;
 
      while Index <= Argument_Count loop
         if Ada.Command_Line.Argument (Index) = "safe" then
            if Got_Safe then
               return;
            end if;
            Got_Safe := True;
            Config_Safe     := True;
            Index := Index + 1;
         elsif Argument (Index) = "count" then
            if Got_Count then
               return;
            end if;
            Got_Count := True;
            Ada.Integer_Text_IO.Get (Item => Config_Count,
                                     From => Argument (Index + 1),
                                     Last => Last);
            if Last /= Argument (Index + 1)'Last then
               return;
            end if;
            Index := Index + 2;
         elsif Argument (Index) = "length" then
            if Got_Length then
               return;
            end if;
            Got_Length := True;
            Ada.Integer_Text_IO.Get (Item => Config_Length,
                                     From => Argument (Index + 1),
                                     Last => Last);
            if Last /= Argument (Index + 1)'Last then
               return;
            end if;
            if Config_Length < 4 then
               return;
            end if;
            Index := Index + 2;
         elsif Argument (Index) = "seed" then
            if Got_Seed then
               return;
            end if;
            Got_Seed := True;
            Ada.Integer_Text_IO.Get (Item => Config_Seed,
                                     From => Argument (Index + 1),
                                     Last => Last);
            if Last /= Argument (Index + 1)'Last then
               return;
            end if;
            Index := Index + 2;
         else
            return;
         end if;
      end loop;
      Success := True;
   exception
      when Constraint_Error | Ada.Text_IO.Data_Error =>
         null;
   end Parse_Command_Line;
 
   function Create_Password (Length : in Positive;
                             Safe   : in Boolean)
                            return String
   is
 
      function Get_Random (Safe : in Boolean)
                          return Character;
 
      function Get_Random (Safe : in Boolean)
                          return Character
      is
         use Ada.Strings.Fixed;
         C : Character;
      begin
         loop
            C := Full_Set (Full_Index_Generator.Random (Index_Generator));
            if Safe then
               exit when Index (Source => Unsafe_Set, Pattern => "" & C) = 0;
            else
               exit;
            end if;
         end loop;
         return C;
      end Get_Random;
 
      function Has_Four (Item : in String)
                        return Boolean;
 
      function Has_Four (Item : in String)
                        return Boolean
      is
         use Ada.Strings.Fixed;
         Has_Upper, Has_Lower : Boolean := False;
         Has_Digit, Has_Spec  : Boolean := False;
      begin
         for C of Item loop
            if Index (Upper_Set, "" & C) /= 0 then Has_Upper := True; end if;
            if Index (Lower_Set, "" & C) /= 0 then Has_Lower := True; end if;
            if Index (Digit_Set, "" & C) /= 0 then Has_Digit := True; end if;
            if Index (Spec_Set,  "" & C) /= 0 then Has_Spec  := True; end if;
         end loop;
         return Has_Upper and Has_Lower and Has_Digit and Has_Spec;
      end Has_Four;
 
      Password : String (1 .. Length);
   begin
      loop
         for I in Password'Range loop
            Password (I) := Get_Random (Safe);
         end loop;
         exit when Has_Four (Password);
      end loop;
      return Password;
   end Create_Password;
 
   Parse_Success : Boolean;
begin
 
   Parse_Command_Line (Parse_Success);
 
   if Config_Show_Help or not Parse_Success then
      Put_Usage;  return;
   end if;
 
   if Config_Seed = 0 then
      Full_Index_Generator.Reset (Index_Generator);
   else
      Full_Index_Generator.Reset (Index_Generator, Config_Seed);
   end if;
 
   for Count in 1 .. Config_Count loop
      Ada.Text_IO.Put_Line (Create_Password (Length => Config_Length,
                                             Safe   => Config_Safe));
   end loop;
 
end Mkpw;