-------------------------------------------------------------------------------
--
--  <STRONG>Copyright &copy; 2001, 2002 by Thomas Wolf.</STRONG>
--  <BLOCKQUOTE>
--    This piece of software is free software; you can redistribute it and/or
--    modify it under the terms of the  GNU General Public License as published
--    by the Free Software  Foundation; either version 2, or (at your option)
--    any later version. This software is distributed in the hope that it will
--    be useful, but <EM>without any warranty</EM>; without even the implied
--    warranty of <EM>merchantability or fitness for a particular purpose.</EM>
--    See the GNU General Public License for  more details. You should have
--    received a copy of the GNU General Public License with this distribution,
--    see file "<A HREF="GPL.txt">GPL.txt</A>". If not, write to the Free
--    Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307,
--    USA.
--  </BLOCKQUOTE>
--  <BLOCKQUOTE>
--    As a special exception from the GPL, if other files instantiate generics
--    from this unit, or you link this unit with other files to produce an
--    executable, this unit does not by itself cause the resulting executable
--    to be covered by the GPL. This exception does not however invalidate any
--    other reasons why the executable file might be covered by the GPL.
--  </BLOCKQUOTE>
--
--  <AUTHOR>
--    Thomas Wolf  (TW) <E_MAIL>
--  </AUTHOR>
--
--  <PURPOSE>
--    Bash-style variable substitution.
--  </PURPOSE>
--
--  <NOT_TASK_SAFE>
--
--  <STORAGE>
--    No dynamic storage allocation. @Expand@ is recursive; the recursion
--    depth (and hence the stack consumption) is limited by the number of
--    variable references and escaped '@$@'-signs in the @Source@ string.
--  </STORAGE>
--
--  <HISTORY>
--    03-MAY-2002   TW  Initial version.
--    14-MAY-2002   TW  Added 'Set_Reference_Character'.
--  </HISTORY>
-------------------------------------------------------------------------------

pragma License (Modified_GPL);

with Util.Strings;

package body Util.Environment.Bash is

   use Util.Strings;

   function Legal_Name
     (Self   : access Bash_Expander;
      Source : in     String)
     return Natural
   is
      pragma Warnings (Off, Self); --  silence -gnatwa
   begin
      return Identifier (Source);
   end Legal_Name;

   function Expand
     (Self   : access Bash_Expander;
      Source : in     String)
     return String
   is

      function Expand
        (Self     : access Bash_Expander;
         Name     : in     String;
         Indirect : in     Boolean)
        return String
      is
      begin
         if Indirect then
            return Indirection (Bash_Expander'Class (Self.all)'Access, Name);
         else
            return Get (Bash_Expander'Class (Self.all)'Access, Name);
         end if;
      end Expand;

      Dollar : Natural;
   begin
      Dollar := Source'First;
      Until_End_Of_String :
      while Dollar <= Source'Last loop
         Dollar := First_Index (Source (Dollar .. Source'Last), Self.Ref_Char);
         exit Until_End_Of_String when Dollar = 0;
         if Dollar > Source'First and then
            Source (Dollar - 1) = '\'
         then
            if Dollar > Source'First + 1 and then
               Source (Dollar - 2) = '\'
            then
               --  The backslash itself is escaped!
               return Source (Source'First .. Dollar - 2) &
                      Expand (Bash_Expander'Class (Self.all)'Access,
                              Source (Dollar .. Source'Last));
            else
               --  Here, we have an escaped dollar sign!
               return Source (Source'First .. Dollar - 2) & Self.Ref_Char &
                      Expand (Bash_Expander'Class (Self.all)'Access,
                              Source (Dollar + 1 .. Source'Last));
            end if;
         end if;
         exit Until_End_Of_String when Dollar = Source'Last;
         if Source (Dollar + 1) = '{' then
            --  Skip ahead to the next '}' at the same nesting level.
            declare
               Start, Stop   : Natural;
               Level         : Natural := 0;
               Op_At         : Natural := 0;
               Var_End       : Natural;
               Default_Start : Natural;
               Indirect      : Boolean := False;
            begin
               if Dollar + 2 <= Source'Length and then
                  Source (Dollar + 2) = '!'
               then
                  Indirect := True;
                  Start := Dollar + 2;
               else
                  Start := Dollar + 1;
               end if;
               Stop := 0;
               for K in Start + 1 .. Source'Last loop
                  if Source (K) = '}' then
                     if Level = 0 then
                        Stop := K; exit;
                     end if;
                     Level := Level - 1;
                  elsif Source (K) = '{' then
                     Level := Level + 1;
                  end if;
               end loop;
               if Stop = 0 then
                  --  No terminating '}' found:
                  Dollar := Dollar + 1;
               elsif Stop = Dollar + 2 then
                  --  "${}" found: replace by the empty string.
                  return Source (Source'First .. Dollar - 1) &
                         Expand (Bash_Expander'Class (Self.all)'Access,
                                 Source (Dollar + 3 .. Source'Last));
               else
                  --  Scan ahead to the next operator at top level.
                  Level := 0;
                  for K in Start + 1 .. Stop - 1 loop
                     if Is_Operator (Bash_Expander'Class (Self.all)'Access,
                                     Source (K))
                     then
                        if Level = 0 then
                           Op_At := K; exit;
                        end if;
                     elsif Source (K) = '{' then
                        Level := Level + 1;
                     elsif Source (K) = '}' then
                        Level := Level - 1;
                     end if;
                  end loop;
                  if Op_At = 0 then
                     --  No operator found:
                     return Source (Source'First .. Dollar - 1) &
                            Expand
                              (Self, Source (Start + 1 .. Stop - 1),
                               Indirect) &
                            Expand
                              (Bash_Expander'Class (Self.all)'Access,
                               Source (Stop + 1 .. Source'Last));
                  else
                     Default_Start := Op_At + 1;
                     --  GNU-bash uses ":-" or ":+". We make the ':' optional,
                     --  since at least Cygwin-bash seems to support this.
                     if Op_At > Start and then Source (Op_At - 1) = ':' then
                        Var_End := Op_At - 2;
                     else
                        Var_End := Op_At - 1;
                     end if;
                     return Source (Source'First .. Dollar - 1) &
                            Execute_Operator
                              (Bash_Expander'Class (Self.all)'Access,
                               Source (Op_At),
                               Source (Start + 1 .. Var_End),
                               Indirect,
                               Source (Default_Start .. Stop - 1)) &
                            Expand
                              (Bash_Expander'Class (Self.all)'Access,
                               Source (Stop + 1 .. Source'Last));
                  end if; --  Operator present?
               end if; --  Terminating '}' found
            end;
         else
            --  '$' not followed by '{'.
            declare
               Last : aliased Natural := 0;
               Def  : constant String :=
                 Prefix (Bash_Expander'Class (Self.all)'Access,
                         Source (Dollar + 1 .. Source'Last),
                         Last'Access);
            begin
               if Last <= Dollar then
                  --  Some error.
                  Dollar := Dollar + 1;
               else
                  return Source (Source'First .. Dollar - 1) &
                         Def &
                         Expand (Bash_Expander'Class (Self.all)'Access,
                                 Source (Last .. Source'Last));
               end if;
            end;
         end if; --  "${" or not?
      end loop Until_End_Of_String;
      --  If we ever get here, we had no expansion.
      return Source;
   end Expand;

   function Indirection
     (Self : access Bash_Expander;
      Name : in     String)
     return String
   is
   begin
      if Name'Last < Name'First or else
         Legal_Name (Bash_Expander'Class (Self.all)'Access, Name) /= Name'Last
      then
         return "";
      end if;
      return Get (Bash_Expander'Class (Self.all)'Access,
                  Get (Bash_Expander'Class (Self.all)'Access, Name));
   end Indirection;

   function Prefix
     (Self   : access Bash_Expander;
      Source : in     String;
      Last   : access Natural)
     return String
   is
   begin
      if Source'Last < Source'First then
         Last.all := Source'First; return "";
      end if;
      Last.all :=
        Legal_Name (Bash_Expander'Class (Self.all)'Access, Source) + 1;
      if Last.all <= Source'First then return ""; end if;
      return Get (Bash_Expander'Class (Self.all)'Access,
                  Source (Source'First .. Last.all - 1));
   end Prefix;

   function Recurse
     (Self   : access Bash_Expander;
      Source : in     String)
     return String
   is
   begin
      return Expand (Bash_Expander'Class (Self.all)'Access, Source);
   end Recurse;

   function Is_Operator
     (Self     : access Bash_Expander;
      Selector : in     Character)
     return Boolean
   is
      pragma Warnings (Off, Self); --  silence -gnatwa
   begin
      return Selector = '+' or else Selector = '-';
   end Is_Operator;

   function Execute_Operator
     (Self         : access Bash_Expander;
      Operator     : in     Character;
      Var_Name     : in     String;
      Indirect     : in     Boolean;
      Default_Part : in     String)
     return String
   is

      function Expand
        (Self     : access Bash_Expander'Class;
         Name     : in     String;
         Indirect : in     Boolean)
        return String
      is
      begin
         if Indirect or else Legal_Name (Self, Name) = Name'Last then
            --  It's not really a recursion
            return Name;
         end if;
         return Recurse (Self, Name);
      end Expand;

      function Expand_Variable
        (Self     : access Bash_Expander'Class;
         Name     : in     String;
         Indirect : in     Boolean)
        return String
      is
      begin
         if Indirect then
            return Indirection (Self, Name);
         else
            return Get (Self, Name);
         end if;
      end Expand_Variable;

      Def  : constant String :=
        Expand_Variable (Bash_Expander'Class (Self.all)'Access,
                         Expand (Bash_Expander'Class (Self.all)'Access,
                                 Var_Name, Indirect),
                         Indirect);
   begin
      if (Def'Last >= Def'First) = (Operator = '-') then
         return Def;
      else
         return Expand (Bash_Expander'Class (Self.all)'Access,
                        Default_Part);
      end if;
   end Execute_Operator;

   procedure Set_Reference_Character
     (Self : access Bash_Expander;
      Char : in     Character)
   is
   begin
      Self.Ref_Char := Char;
   end Set_Reference_Character;

end Util.Environment.Bash;
