#!/usr/local/bin/perl -w

#@(#) OA96 <Olivier.Aubert@enst-bretagne.fr>
#@(#) csh to sh converter

# This script is distributed under the GNU General Public License
# version 2.1 or later. See http://www.fsf.org/

# This program 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.1, or (at your option)
# any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# A copy of the GNU General Public License can be obtained from this
# program's author (send electronic mail to the above address) or from
# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
# or from the Free Software Foundation website: http://www.fsf.org/

require 5.000;
use strict;

# Misc
$C2S::RCSversion = q$Revision: 1.6 $;
($C2S::version) = ($C2S::RCSversion =~ /Revision:\s+([\d]+\.[\d]+)/);

# Already exported variables
# There's no point in exporting them more than once
%C2S::exported  = ();

$C2S::errors = 0;

# List of defined variables (in the script)
%C2S::var = ();

# Various various
$C2S::iflevel   = 0;
$C2S::caselevel = 0;
$C2S::dolevel   = 0;
my $indent    = "";

# Directory where we put the converted source'd files
$C2S::external_dir = $ENV{HOME}."/lib/csh2sh";

# csh builtins that we don't know how to translate
%C2S::unknown_builtin = map( ($_, 1), 
                             qw(
                                dirs glob goto hashstat
                                history jobs logout notify
                                onintr popd pushd repeat stop
                                suspend unalias unhash unlimit
                                wait
                               )
                           );

# Limit->ulimit conversion
%C2S::limit = qw(
                 coredumpsize -c
                 filesize -f
                 datasize -d
                 cputime -t
                 stacksize -s
                 descriptors -n
                 memorysize -v
                );

# Let's go...

$C2S::fichier = shift(@ARGV) || &usage;

$C2S::sortie = shift(@ARGV) || '-';

open(SORTIE, ">$C2S::sortie") or die "Unable to create $C2S::sortie: $!\n";

open(F, $C2S::fichier) or die "Unable to open $C2S::fichier: $!\n";

warn "Converting $C2S::fichier\n";

my $line;
my $temp = "";

while (defined($line = <F>))
{
  # convert "#!/bin/csh\s+-f" or "#" to "#!/bin/sh"
  $line =~ s,^#!\s*/bin/csh( -f)?,#!/bin/sh,;

  # We do not modify empty lines or comments
  if ($line =~ /^\s*\#/ || $line =~ /^\s*$/)
  {
    print SORTIE $line;
    next;
  }
  
  chomp($line);

  # Strip trailing spaces
  $line =~ s/\s+$//;
  if ($line =~ /\\$/)
  {
    $temp .= $line . "\n";
    next;
  }
  else
  {
    $temp .= $line;
    # We strip leading spaces and memorize the indentation
    $indent = $1 if $temp =~ s/^(\s*)//;
    print SORTIE &convert_line($temp);
    $temp = "";
  }
}
close(F);

warn "$C2S::fichier -- missing endif\n" if ($C2S::iflevel != 0);
warn "$C2S::fichier -- missing endsw\n" if ($C2S::caselevel != 0);
warn "$C2S::fichier -- missing end\n" if ($C2S::dolevel != 0);
warn "$C2S::fichier successfully converted.\n" if $C2S::errors == 0;
warn "$C2S::errors errors.\n" if ($C2S::errors != 0);
exit;

sub convert_line
{
  my $line = shift;
  my $res = "";
  
  # Environment variables
  if ($line =~ /^setenv\s+(.+?)\s+(.+)$/s)
  {
    my($nom, $valeur);
    
    $nom = $1;
    $valeur = $2;
    $C2S::var{$nom} = $valeur;
    
    $res = &indente($nom, "=", $valeur);
    
    if (!defined($C2S::exported{$nom}))
    {
      $C2S::exported{$nom} = 1;
      $res .= &indente("export ", $nom);
    }
    return $res;
  }
  # aliases
  elsif ($line =~ /^alias\s+(.+?)\s+(.+)$/s)
  {
    my($nom, $alias);
    $nom = $1;
    $alias = $2;
    
    $alias =~ s/^\'?(.+?)\'?$/$1/;
    $alias =~ s/^\"?(.+?)\"?$/$1/;
    $alias =~ s/\\!/!/g;
    
    if ($alias =~ /\b\\\![1-9*]\b/)
    {
      $alias =~ s/\\\!([1-9*])/\$$1/g;
    }
    
    return &indente($nom, " () { ", $alias, " }");
  }
  # Non exported variables
  elsif ($line =~ /^set\s+(.+?)\s*=\s*(.+?)$/s)
  {
    my($nom, $valeur);
    $nom    = $1;
    $valeur = $2;
    $C2S::var{$nom} = $valeur;
    
    if ($nom eq "path")
    {
      # Special case for the "path" variable: we have to deal with the ugly
      # csh correspondance between path and PATH
      # We could do it for term too.
      $valeur =~ s/^.*\(\s*//s;
      $valeur =~ s/\s*\).*$//s;
      $valeur =~ tr/ /:/;
      $valeur =~ s/\$path(:|$)/\$PATH$1/s;
      
      $res = &indente("PATH=", $valeur);
      if (!defined($C2S::exported{"PATH"}))
      {
        $C2S::exported{"PATH"} = 1;
        $res .= &indente("export PATH");
      }
      return $res;                    
    }
    # csh options
    elsif ($nom =~ /^-./)
    {
      # We do not (yet?) handle csh options
      return &unable($line);
    }
    # User input
    elsif ($valeur eq '$<')
    {
      return &indente("read ", $nom);
    }
    else
    {
      return &indente($nom, "=", $valeur);
    }
  }
  # csh option
  elsif ($line =~ /^set\s/)
  {
    # It must be a csh option
    return &unable($line);
  }
  elsif ($line =~ /^if/)
  {
    return &do_if($line);
  }
  elsif ($line =~ /^else\s+if/)
  {
    return &do_elsif($line);
  }
  elsif ($line =~ /^else/)
  {
    $res = &unable($line) if ($C2S::iflevel == 0);
    $res .= &indente("else");
    return $res;
  }
  elsif ($line =~ /^endif/)
  {
    $res = &unable($line) if ($C2S::iflevel == 0);
    $res .= &indente("fi");
    $C2S::iflevel--;
    return $res;
  }
  elsif ($line =~ /^rehash/)
  {
    return &indente("hash -r");
  }
  elsif ($line =~ /^(unset|unsetenv)\s+(.+)/s)
  {
    my $nom;
    $nom = $2;
    return &indente("unset ", $nom);
  }
  elsif ($line =~ /^foreach\s+(\w\S*)\s+\((.+)\)/s)
  {
    my($var, $list);
    $var  = $1;
    $list = $2;
    $res = &indente("for $var in $list");
    $res .= &indente("do");
    $C2S::dolevel++;
    return $res;
  }
  elsif ($line =~ /^end$/)
  {
    $res = &unable($line) if $C2S::dolevel == 0;
    $res .= &indente("done");
    $C2S::dolevel--;
    return $res;
  }
  elsif ($line =~ /^switch\s*\((.+?)\)/s)
  {
    my $valeur;
    $valeur = $1;
    # These commented lines are useless... But why did I put them in
    # the first place ????
#    if ($valeur =~ /^\s*\$\?(.+?)\s*$/ || $valeur =~ /^\s*\$\{\?(.+)\}\s*$/)
#    {
#      $nom = $1;
#      $res = &indente('test ! "${', $nom, '}"');
#      $valeur = '$?';
#    }
    $res .= &indente("case ", $valeur, " in");
    $C2S::caselevel++;
    return $res;
  }
  elsif ($line =~ /^case\s*(.+?):?$/)
  {
    $res = &unable($line) if $C2S::caselevel == 0;
    $res .= &indente($1, ")");
    return $res;
  }
  elsif ($line =~ /^breaksw/)
  {
    $res = &unable($line) if $C2S::caselevel == 0;
    $res .= &indente(";;");
    return $res;
  }
  elsif ($line =~ /^endsw/)
  {
    $res = &unable($line) if $C2S::caselevel == 0;
    $res .= &indente("esac");
    $C2S::caselevel--;
    return $res;
  }
  elsif ($line =~ /^limit\s+(\S+)\s*(\S*)/)
  {
    my $flag = $1;
    my $arg = $2;
    
    if (! defined $C2S::limit{$flag})
    {
      $res = &unable($line);
    }
    else
    {
      # Convert the argument in the right unit.
      if ($arg =~ /^(\d+)k$/)
      {
        $arg = $1;
      }
      elsif ($arg =~ /^(\d+)m$/)
      {
        if ($flag eq "cputime")
        {
          $arg = $1 * 60;
        }
        else
        {
          $arg = $1 * 1024;
        }
      }
      elsif ($arg =~ /^(\d+)h$/)
      {
        $arg = $1 * 3600;
      }
      elsif ($arg =~ /^(\d+):(\d+)$/)
      {
        $arg = 60 * $1 + $2;
      }
      
      # Convert args into 512 bytes blocks for -c and -f
      $arg /= 2 if ($flag eq "coredumpsize" or $flag eq "filesize");
      
      $res = &indente("ulimit " . $C2S::limit{$flag} . " " . $arg);
    }
    return $res;
  }
  elsif ($line =~ /^source\b/)
  {
    my $file;
    $file = &external_prog($line);
    if ($file ne "")
    {
      return &unable($line);
    }
    else
    {
      return &indente(". $file");
    }
  }
  elsif ($line =~ /^default:/)
  { 
    $res = &unable($line) if $C2S::caselevel == 0;
    $res .= &indente("*)");
    return $res;
  }
  elsif ($line =~ /^eval\s+(.+)/s)
  {
    $res = "eval " . &convert_line($1);
    return &indente($res);
  }
  elsif ($line =~ /^(\w+)/ && defined $C2S::unknown_builtin{$1})
  {
    # We don't know how to convert this line.
    return &unable($line);
  }
  else
  {
    # Output redirections
    $line =~ s/>\&\s*(\S+)/> $1 2>\&1/;
    $line =~ s/\|\&/2>\&1 |/;
    
    return &indente($line);
  }
}
    
sub unable
{
  my($s) = shift;
  my $res = "";
  
  $res = &indente('echo "Manual fix needed" && exit');
  $res .= &indente("# !!! Unable to convert the following line:");
  $res .= &indente("# ".$s);

  $C2S::errors++;
  
  warn "$C2S::fichier -- Unable to convert the line: $s\n";
  $res;
}

sub do_if
{
  my($line) = shift;
  my($test, $action);
  my $res = "";
  
  if ($line =~ /if\s*\((.+?)\)\s+then/)
  {
    $test = $1;
    $res = &indente("if [ ", &convert_test($test), " ]");
    $res .= &indente("then");
    $C2S::iflevel++;
  }
  elsif ($line =~ /if\s*\((.+?)\)\s+(.+)/)
  {
    $test = $1;
    $action = $2;
    $res = &indente("[ ", &convert_test($test), " ] && ",
                    &convert_line($action));
  }
  return $res;
}

sub do_elsif
{
  my($line) = shift;
  my $res = "";
  my($test, $action);
  
  if ($line =~ /else\s+if\s*\((.+?)\)\s+then/)
  {
    $test = $1;
    $res = &indente("elif [ ", &convert_test($test), " ]");
    $res .= &indente("then");
    $C2S::iflevel++;
  }
  elsif ($line =~ /else\s+if\s*\((.+?)\)\s+(.+)/)
  {
    $test = $1;
    $action = $2;
    $res = &indente("elif [ ", &convert_test($test), " ]");
    $res .= &indente("then");
    $res .= &convert_line($action);
    $res .= &indente("fi");
  }
  return $res;
}

sub convert_test
{
  my($test) = shift;
  my($res) = "";
  my(@test);
  my $nom;
  my $warning = '**impossible**';
  
  my %transl_tests = ("-r" => "-r",
                      "-w" => "-w",
                      "-x" => "-x",
                      "-e" => "-f",
                      "-o" => "-w", # Not exactly, but it should do the trick
                      "-z" => "! -s",
                      "-f" => "-f",
                      "-d" => "-d",
                      "==" => "=",
                      "!=" => "!=",
                      ">"  => "-gt",
                      ">=" => "-ge",
                      "<"  => "-lt",
                      "<=" => "-le",
                      "||" => "-o",
                      "&&" => "-a",
                      "=~" => $warning,
                      "!~" => $warning,
                     );

  # Numerical test on a variable (variables in cshell *must* start with
  # a letter, or so I'm told by the FM)
  if ($test =~ /^\$(\w\S*)$/)
  {
    $res = $1;
    $res =~ s/^\{(.+)\}$/$1/;
    $res = ' ${' . $res . '} -gt 0 ';
    return $res;
  }

  @test = split(/\s+/, $test);

  my $i;
  
  for $i (@test)
  {
    if (defined($transl_tests{$i}))
    {
      if ($transl_tests{$i} eq $warning)
      {
        $res .= "\n" . &unable($i);
      }
      else
      {
        $res .= " " . $transl_tests{$i} . " ";
      }
    }
    # Test if a variable is set
    elsif ($i =~ /^\$\?(.+)$/ || $i =~ /^\$\{\?(.+)\}$/)
    {
      $nom = $1;
      # This approximation should work in most cases
      $res .= ' -n "${' . $nom . '}" ';
    }
    # Variable name
    elsif ($i =~ /^\$(.+)$/)
    {
      $nom = $1;
      $nom =~ s/^\{(.+)\}/$1/;
      $res .= ' "${' . $nom . '}" ';
    }
    # Anything else
    else
    {
      $i =~ s|^~/|\${HOME}/|;
      $res .= " " . $i . " ";
    }
  }
  $res;
}

sub indente
{
  return $indent . join("", @_) . "\n";
}

sub external_prog
{
  my $line = shift;

  my $file = "";
  my $orig;
  my $basename;
  my $nom;
  
  ($orig) = ($line =~ m|^\s*source\s+(.+)|);

#  $orig =~ s|\$(\w[\w\d]*)|$var{$1}|g;
  while ($orig =~ m|\$(\w[\w\d]*)|g)
  {
    $nom = $1;
    if (defined $C2S::var{$nom})
    {
      $orig =~ s|\$$nom|$C2S::var{$nom}|;
    }
    else
    {
      return "";
    }
  }

  ($basename) = ($orig =~ m|/([^\/]+)$|);

  warn "Converting $orig to $basename...\n";
  system($0, $orig, join("/", $C2S::external_dir, $basename));
  
  return join("/", $C2S::external_dir, $basename);
}

sub usage
{
  print <<"EOF";
csh2sh version $C2S::version

Syntax:
 csh2sh input_file [output_file]

output goes to stdout if output_file is not given.
Warnings go to stderr.

Don't expect too much from it. It will handle simple variable settings
and simple tests rather well, but nothing fancier. 

As always, comments, criticisms and patches are welcome

OA96 <Olivier.Aubert\@enst-bretagne.fr>
EOF
;
  exit 0;
}

__END__

