1package Shell::Config::Generate;
2
3use strict;
4use warnings;
5use 5.008001;
6use Shell::Guess;
7use Carp qw( croak );
8use Exporter ();
9
10# ABSTRACT: Portably generate config for any shell
11our $VERSION = '0.34'; # VERSION
12
13
14sub new
15{
16  my($class) = @_;
17  bless { commands => [], echo_off => 0 }, $class;
18}
19
20
21sub set
22{
23  my($self, $name, $value) = @_;
24
25  push @{ $self->{commands} }, ['set', $name, $value];
26
27  $self;
28}
29
30
31sub set_path
32{
33  my($self, $name, @list) = @_;
34
35  push @{ $self->{commands} }, [ 'set_path', $name, @list ];
36
37  $self;
38}
39
40
41sub append_path
42{
43  my($self, $name, @list) = @_;
44
45  push @{ $self->{commands} }, [ 'append_path', $name, @list ]
46    if @list > 0;
47
48  $self;
49}
50
51
52sub prepend_path
53{
54  my($self, $name, @list) = @_;
55
56  push @{ $self->{commands} }, [ 'prepend_path', $name, @list ]
57    if @list > 0;
58
59  $self;
60}
61
62
63sub comment
64{
65  my($self, @comments) = @_;
66
67  push @{ $self->{commands} }, ['comment', $_] for @comments;
68
69  $self;
70}
71
72
73sub shebang
74{
75  my($self, $location) = @_;
76  $self->{shebang} = $location;
77  $self;
78}
79
80
81sub echo_off
82{
83  my($self) = @_;
84  $self->{echo_off} = 1;
85  $self;
86}
87
88
89sub echo_on
90{
91  my($self) = @_;
92  $self->{echo_off} = 0;
93  $self;
94}
95
96sub _value_escape_csh
97{
98  my $value = shift() . '';
99  $value =~ s/([\n!])/\\$1/g;
100  $value =~ s/(')/'"$1"'/g;
101  $value;
102}
103
104sub _value_escape_fish
105{
106  my $value = shift() . '';
107  $value =~ s/([\n])/\\$1/g;
108  $value =~ s/(')/'"$1"'/g;
109  $value;
110}
111
112sub _value_escape_sh
113{
114  my $value = shift() . '';
115  $value =~ s/(')/'"$1"'/g;
116  $value;
117}
118
119sub _value_escape_win32
120{
121  my $value = shift() . '';
122  $value =~ s/%/%%/g;
123  $value =~ s/([&^|<>()])/^$1/g;
124  $value =~ s/\n/^\n\n/g;
125  $value;
126}
127
128#   `0  Null
129#   `a  Alert bell/beep
130#   `b  Backspace
131#   `f  Form feed (use with printer output)
132#   `n  New line
133#   `r  Carriage return
134# `r`n  Carriage return + New line
135#   `t  Horizontal tab
136#   `v  Vertical tab (use with printer output)
137
138my %ps = ( # microsoft would have to be different
139  "\0" => '`0',
140  "\a" => '`a',
141  "\b" => '`b',
142  "\f" => '`f',
143  "\r" => '`r',
144  "\n" => '`n',
145  "\t" => '`t',
146  #"\v" => '`v',
147);
148
149sub _value_escape_powershell
150{
151  my $value = shift() . '';
152  $value =~ s/(["'`\$#()])/`$1/g;
153  $value =~ s/([\0\a\b\f\r\n\t])/$ps{$1}/eg;
154  $value;
155}
156
157
158sub set_alias
159{
160  my($self, $alias, $command) = @_;
161
162  push @{ $self->{commands} }, ['alias', $alias, $command];
163}
164
165
166sub set_path_sep
167{
168  my($self, $sep) = @_;
169  push @{ $self->{commands} }, ['set_path_sep', $sep];
170}
171
172
173sub generate
174{
175  my($self, $shell) = @_;
176
177  if(defined $shell)
178  {
179    if(ref($shell) eq '')
180    {
181      my $method = join '_', $shell, 'shell';
182      if(Shell::Guess->can($method))
183      {
184        $shell = Shell::Guess->$method;
185      }
186      else
187      {
188        croak("unknown shell type: $shell");
189      }
190    }
191  }
192  else
193  {
194    $shell = Shell::Guess->running_shell;
195  }
196
197  $self->_generate($shell);
198}
199
200sub _generate
201{
202  my($self, $shell) = @_;
203
204  my $buffer = '';
205  my $sep    = $shell->is_win32 ? ';' : ':';
206
207  if(exists $self->{shebang} && $shell->is_unix)
208  {
209    if(defined $self->{shebang})
210    { $buffer .= "#!" . $self->{shebang} . "\n" }
211    else
212    { $buffer .= "#!" . $shell->default_location . "\n" }
213  }
214
215  if($self->{echo_off} && ($shell->is_cmd || $shell->is_command))
216  {
217    $buffer .= '@echo off' . "\n";
218  }
219
220  foreach my $args (map { [@$_] } @{ $self->{commands} })
221  {
222    my $command = shift @$args;
223
224    if($command eq 'set_path_sep')
225    {
226      $sep = shift @$args;
227      next;
228    }
229
230    # rewrite set_path as set
231    if($command eq 'set_path')
232    {
233      $command = 'set';
234      my $name = shift @$args;
235      $args = [$name, join $sep, @$args];
236    }
237
238    if($command eq 'set')
239    {
240      my($name, $value) = @$args;
241      if($shell->is_c)
242      {
243        $value = _value_escape_csh($value);
244        $buffer .= "setenv $name '$value';\n";
245      }
246      elsif($shell->is_fish)
247      {
248        $value = _value_escape_fish($value);
249        $buffer .= "set -x $name '$value';\n";
250      }
251      elsif($shell->is_bourne)
252      {
253        $value = _value_escape_sh($value);
254        $buffer .= "$name='$value';\n";
255        $buffer .= "export $name;\n";
256      }
257      elsif($shell->is_cmd || $shell->is_command)
258      {
259        $value = _value_escape_win32($value);
260        $buffer .= "set $name=$value\n";
261      }
262      elsif($shell->is_power)
263      {
264        $value = _value_escape_powershell($value);
265        $buffer .= "\$env:$name = \"$value\"\n";
266      }
267      else
268      {
269        croak 'don\'t know how to "set" with ' . $shell->name;
270      }
271    }
272
273    elsif($command eq 'append_path' || $command eq 'prepend_path')
274    {
275      my($name, @values) = @$args;
276      if($shell->is_c)
277      {
278        my $value = join $sep, map { _value_escape_csh($_) } @values;
279        $buffer .= "test \"\$?$name\" = 0 && setenv $name '$value' || ";
280        if($command eq 'prepend_path')
281        { $buffer .= "setenv $name '$value$sep'\"\$$name\"" }
282        else
283        { $buffer .= "setenv $name \"\$$name\"'$sep$value'" }
284        $buffer .= ";\n";
285      }
286      elsif($shell->is_bourne)
287      {
288        my $value = join $sep, map { _value_escape_sh($_) } @values;
289        $buffer .= "if [ -n \"\$$name\" ] ; then\n";
290        if($command eq 'prepend_path')
291        { $buffer .= "  $name='$value$sep'\$$name;\n  export $name;\n" }
292        else
293        { $buffer .= "  $name=\$$name'$sep$value';\n  export $name\n" }
294        $buffer .= "else\n";
295        $buffer .= "  $name='$value';\n  export $name;\n";
296        $buffer .= "fi;\n";
297      }
298      elsif($shell->is_fish)
299      {
300        my $value = join ' ', map { _value_escape_fish($_) } @values;
301        $buffer .= "if [ \"\$$name\" == \"\" ]; set -x $name $value; else; ";
302        if($command eq 'prepend_path')
303        { $buffer .= "set -x $name $value \$$name;" }
304        else
305        { $buffer .= "set -x $name \$$name $value;" }
306        $buffer .= "end\n";
307      }
308      elsif($shell->is_cmd || $shell->is_command || $shell->is_power)
309      {
310        my $value = join $sep, map { $shell->is_power ? _value_escape_powershell($_) : _value_escape_win32($_) } @values;
311        if($shell->is_power)
312        {
313          $buffer .= "if(\$env:$name) { ";
314          if($command eq 'prepend_path')
315          { $buffer .= "\$env:$name = \"$value$sep\" + \$env:$name" }
316          else
317          { $buffer .= "\$env:$name = \$env:$name + \"$sep$value\"" }
318          $buffer .= " } else { \$env:$name = \"$value\" }\n";
319        }
320        else
321        {
322          $buffer .= "if defined $name (set ";
323          if($command eq 'prepend_path')
324          { $buffer .= "$name=$value$sep%$name%" }
325          else
326          { $buffer .= "$name=%$name%$sep$value" }
327          $buffer .=") else (set $name=$value)\n";
328        }
329      }
330      else
331      {
332        croak 'don\'t know how to "append_path" with ' . $shell->name;
333      }
334    }
335
336    elsif($command eq 'comment')
337    {
338      if($shell->is_unix || $shell->is_power)
339      {
340        $buffer .= "# $_\n" for map { split /\n/, } @$args;
341      }
342      elsif($shell->is_cmd || $shell->is_command)
343      {
344        $buffer .= "rem $_\n" for map { split /\n/, } @$args;
345      }
346      else
347      {
348        croak 'don\'t know how to "comment" with ' . $shell->name;
349      }
350    }
351
352    elsif($command eq 'alias')
353    {
354      if($shell->is_bourne)
355      {
356        $buffer .= "alias $args->[0]=\"$args->[1]\";\n";
357      }
358      elsif($shell->is_c)
359      {
360        $buffer .= "alias $args->[0] $args->[1];\n";
361      }
362      elsif($shell->is_cmd || $shell->is_command)
363      {
364        $buffer .= "DOSKEY $args->[0]=$args->[1] \$*\n";
365      }
366      elsif($shell->is_power)
367      {
368        $buffer .= sprintf("function %s { %s \$args }\n", $args->[0], _value_escape_powershell($args->[1]));
369      }
370      elsif($shell->is_fish)
371      {
372        $buffer .= "alias $args->[0] '$args->[1]';\n";
373      }
374      else
375      {
376        croak 'don\'t know how to "alias" with ' . $shell->name;
377      }
378    }
379  }
380
381  $buffer;
382}
383
384
385sub generate_file
386{
387  my($self, $shell, $filename) = @_;
388  my $fh;
389  open($fh, '>', $filename) or die "cannot open $filename: $!";
390  print $fh $self->generate($shell) or die "cannot write $filename: $!";
391  close $fh or die "error closing $filename: $!";
392}
393
394*import = \&Exporter::import;
395
396our @EXPORT_OK = qw( win32_space_be_gone cmd_escape_path powershell_escape_path );
397
398
399*_win_to_posix_path = $^O =~ /^(cygwin|msys)$/ ? \&Cygwin::win_to_posix_path : sub { $_[0] };
400*_posix_to_win_path = $^O =~ /^(cygwin|msys)$/ ? \&Cygwin::posix_to_win_path : sub { $_[0] };
401
402sub win32_space_be_gone
403{
404  return @_ if $^O !~ /^(MSWin32|cygwin|msys)$/;
405  map { /\s/ ? _win_to_posix_path(Win32::GetShortPathName(_posix_to_win_path($_))) : $_ } @_;
406}
407
408
409sub cmd_escape_path
410{
411  my $path = shift() . '';
412  $path =~ s/%/%%/g;
413  $path =~ s/([&^|<>])/^$1/g;
414  $path =~ s/\n/^\n\n/g;
415  "\"$path\"";
416}
417
418
419sub powershell_escape_path
420{
421  map { my $p = _value_escape_powershell($_); $p =~ s/ /` /g; $p } @_;
422}
423
4241;
425
426__END__
427
428=pod
429
430=encoding UTF-8
431
432=head1 NAME
433
434Shell::Config::Generate - Portably generate config for any shell
435
436=head1 VERSION
437
438version 0.34
439
440=head1 SYNOPSIS
441
442With this start up:
443
444 use Shell::Guess;
445 use Shell::Config::Generate;
446
447 my $config = Shell::Config::Generate->new;
448 $config->comment( 'this is my config file' );
449 $config->set( FOO => 'bar' );
450 $config->set_path(
451   PERL5LIB => '/foo/bar/lib/perl5',
452               '/foo/bar/lib/perl5/perl5/site',
453 );
454 $config->append_path(
455   PATH => '/foo/bar/bin',
456           '/bar/foo/bin',
457 );
458
459This:
460
461 $config->generate_file(Shell::Guess->bourne_shell, 'config.sh');
462
463will generate a config.sh file with this:
464
465 # this is my config file
466 FOO='bar';
467 export FOO;
468 PERL5LIB='/foo/bar/lib/perl5:/foo/bar/lib/perl5/perl5/site';
469 export PERL5LIB;
470 if [ -n "$PATH" ] ; then
471   PATH=$PATH:'/foo/bar/bin:/bar/foo/bin';
472   export PATH
473 else
474   PATH='/foo/bar/bin:/bar/foo/bin';
475   export PATH;
476 fi;
477
478and this:
479
480 $config->generate_file(Shell::Guess->c_shell, 'config.csh');
481
482will generate a config.csh with this:
483
484 # this is my config file
485 setenv FOO 'bar';
486 setenv PERL5LIB '/foo/bar/lib/perl5:/foo/bar/lib/perl5/perl5/site';
487 test "$?PATH" = 0 && setenv PATH '/foo/bar/bin:/bar/foo/bin' || setenv PATH "$PATH":'/foo/bar/bin:/bar/foo/bin';
488
489and this:
490
491 $config->generate_file(Shell::Guess->cmd_shell, 'config.cmd');
492
493will generate a C<config.cmd> (Windows C<cmd.exe> script) with this:
494
495 rem this is my config file
496 set FOO=bar
497 set PERL5LIB=/foo/bar/lib/perl5;/foo/bar/lib/perl5/perl5/site
498 if defined PATH (set PATH=%PATH%;/foo/bar/bin;/bar/foo/bin) else (set PATH=/foo/bar/bin;/bar/foo/bin)
499
500=head1 DESCRIPTION
501
502This module provides an interface for specifying shell configurations
503for different shell environments without having to worry about the
504arcane differences between shells such as csh, sh, cmd.exe and command.com.
505
506It does not modify the current environment, but it can be used to
507create shell configurations which do modify the environment.
508
509This module uses L<Shell::Guess> to represent the different types
510of shells that are supported.  In this way you can statically specify
511just one or more shells:
512
513 #!/usr/bin/perl
514 use Shell::Guess;
515 use Shell::Config::Generate;
516 my $config = Shell::Config::Generate->new;
517 # ... config config ...
518 $config->generate_file(Shell::Guess->bourne_shell,  'foo.sh' );
519 $config->generate_file(Shell::Guess->c_shell,       'foo.csh');
520 $config->generate_file(Shell::Guess->cmd_shell,     'foo.cmd');
521 $config->generate_file(Shell::Guess->command_shell, 'foo.bat');
522
523This will create foo.sh and foo.csh versions of the configurations,
524which can be sourced like so:
525
526 #!/bin/sh
527 . ./foo.sh
528
529or
530
531 #!/bin/csh
532 source foo.csh
533
534It also creates C<.cmd> and C<.bat> files with the same configuration
535which can be used in Windows.  The configuration can be imported back
536into your shell by simply executing these files:
537
538 C:\> foo.cmd
539
540or
541
542 C:\> foo.bat
543
544Alternatively you can use the shell that called your Perl script using
545L<Shell::Guess>'s C<running_shell> method, and write the output to
546standard out.
547
548 #!/usr/bin/perl
549 use Shell::Guess;
550 use Shell::Config::Generate;
551 my $config = Shell::Config::Generate->new;
552 # ... config config ...
553 print $config->generate(Shell::Guess->running_shell);
554
555If you use this pattern, you can eval the output of your script using
556your shell's back ticks to import the configuration into the shell.
557
558 #!/bin/sh
559 eval `script.pl`
560
561or
562
563 #!/bin/csh
564 eval `script.pl`
565
566=head1 CONSTRUCTOR
567
568=head2 new
569
570 my $config = Shell::Config::Generate->new;
571
572creates an instance of She::Config::Generate.
573
574=head1 METHODS
575
576There are two types of instance methods for this class:
577
578=over 4
579
580=item * modifiers
581
582adjust the configuration in an internal portable format
583
584=item * generators
585
586generate shell configuration in a specific format given
587the internal portable format stored inside the instance.
588
589=back
590
591The idea is that you can create multiple modifications
592to the environment without worrying about specific shells,
593then when you are done you can create shell specific
594versions of those modifications using the generators.
595
596This may be useful for system administrators that must support
597users that use different shells, with a single configuration
598generation script written in Perl.
599
600=head2 set
601
602 $config->set( $name => $value );
603
604Set an environment variable.
605
606=head2 set_path
607
608 $config->set_path( $name => @values );
609
610Sets an environment variable which is stored in standard
611'path' format (Like PATH or PERL5LIB).  In UNIX land this
612is a colon separated list stored as a string.  In Windows
613this is a semicolon separated list stored as a string.
614You can do the same thing using the C<set> method, but if
615you do so you have to determine the correct separator.
616
617This will replace the existing path value if it already
618exists.
619
620=head2 append_path
621
622 $config->append_path( $name => @values );
623
624Appends to an environment variable which is stored in standard
625'path' format.  This will create a new environment variable if
626it doesn't already exist, or add to an existing value.
627
628=head2 prepend_path
629
630 $config->prepend_path( $name => @values );
631
632Prepend to an environment variable which is stored in standard
633'path' format.  This will create a new environment variable if
634it doesn't already exist, or add to an existing value.
635
636=head2 comment
637
638 $config->comment( $comment );
639
640This will generate a comment in the appropriate format.
641
642B<note> that including comments in your configuration may mean
643it will not work with the C<eval> backticks method for importing
644configurations into your shell.
645
646=head2 shebang
647
648 $config->shebang;
649 $config->shebang($location);
650
651This will generate a shebang at the beginning of the configuration,
652making it appropriate for use as a script.  For non UNIX shells this
653will be ignored.  If specified, C<$location> will be used as the
654interpreter location.  If it is not specified, then the default
655location for the shell will be used.
656
657B<note> that the shebang in your configuration may mean
658it will not work with the C<eval> backticks method for importing
659configurations into your shell.
660
661=head2 echo_off
662
663 $config->echo_off;
664
665For DOS/Windows configurations (C<command.com> or C<cmd.exe>), issue this as the
666first line of the config:
667
668 @echo off
669
670=head2 echo_on
671
672 $config->echo_on;
673
674Turn off the echo off (that is do not put anything at the beginning of
675the config) for DOS/Windows configurations (C<command.com> or C<cmd.exe>).
676
677=head2 set_alias
678
679 $config->set_alias( $alias => $command )
680
681Sets the given alias to the given command.
682
683Caveat:
684some older shells do not support aliases, such as
685the original bourne shell.  This module will generate
686aliases for those shells anyway, since /bin/sh may
687actually be a more modern shell that DOES support
688aliases, so do not use this method unless you can be
689reasonable sure that the shell you are generating
690supports aliases.  On Windows, for PowerShell, a simple
691function is used instead of an alias so that arguments
692may be specified.
693
694=head2 set_path_sep
695
696 $config->set_path_sep( $sep );
697
698Use C<$sep> as the path separator instead of the shell
699default path separator (generally C<:> for Unix shells
700and C<;> for Windows shells).
701
702Not all characters are supported, it is usually best
703to stick with the shell default or to use C<:> or C<;>.
704
705=head2 generate
706
707 my $command_text = $config->generate;
708 my $command_text = $config->generate( $shell );
709
710Generate shell configuration code for the given shell.
711C<$shell> is an instance of L<Shell::Guess>.  If C<$shell>
712is not provided, then this method will use Shell::Guess
713to guess the shell that called your perl script.
714
715You can also pass in the shell name as a string for
716C<$shell>.  This should correspond to the appropriate
717I<name>_shell from L<Shell::Guess>.  So for csh you
718would pass in C<"c"> and for tcsh you would pass in
719C<"tc">, etc.
720
721=head2 generate_file
722
723 $config->generate_file( $shell, $filename );
724
725Generate shell configuration code for the given shell
726and write it to the given file.  C<$shell> is an instance
727of L<Shell::Guess>.  If there is an IO error it will throw
728an exception.
729
730=head1 FUNCTIONS
731
732=head2 win32_space_be_gone
733
734 my @new_path_list = win32_space_be_gone( @orig_path_list );
735
736On C<MSWin32> and C<cygwin>:
737
738Given a list of directory paths (or filenames), this will
739return an equivalent list of paths pointing to the same
740file system objects without spaces.  To do this
741C<Win32::GetShortPathName()> is used on to find alternative
742path names without spaces.
743
744NOTE that this breaks when Windows is told not to create
745short (C<8+3>) filenames; see L<http://www.perlmonks.org/?node_id=333930>
746for a discussion of this behaviour.
747
748In addition, on just C<Cygwin>:
749
750The input paths are first converted from POSIX to Windows paths
751using C<Cygwin::posix_to_win_path>, and then converted back to
752POSIX paths using C<Cygwin::win_to_posix_path>.
753
754Elsewhere:
755
756Returns the same list passed into it
757
758=head2 cmd_escape_path
759
760 my @new_path_list = cmd_escape_path( @orig_path_list )
761
762Given a list of directory paths (or filenames), this will
763return an equivalent list of paths escaped for cmd.exe and command.com.
764
765=head2 powershell_escape_path
766
767 my @new_path_list = powershell_escape_path( @orig_path_list )
768
769Given a list of directory paths (or filenames), this will
770return an equivalent list of paths escaped for PowerShell.
771
772=head1 CAVEATS
773
774The test suite tests this module's output against the actual
775shells that should understand them, if they can be found in
776the path.  You can generate configurations for shells which
777are not available (for example C<cmd.exe> configurations from UNIX or
778bourne configurations under windows), but the test suite only tests
779them if they are found during the build of this module.
780
781The implementation for C<csh> depends on the external command C<test>.
782As far as I can tell C<test> should be available on all modern
783flavors of UNIX which are using C<csh>.  If anyone can figure out
784how to prepend or append to path type environment variable without
785an external command in C<csh>, then a patch would be appreciated.
786
787The incantation for prepending and appending elements to a path
788on csh probably deserve a comment here.  It looks like this:
789
790 test "$?PATH" = 0 && setenv PATH '/foo/bar/bin:/bar/foo/bin' || setenv PATH "$PATH":'/foo/bar/bin:/bar/foo/bin';
791
792=over 4
793
794=item * one line
795
796The command is all on one line, and doesn't use if, which is
797probably more clear and ideomatic.  This for example, might
798make more sense:
799
800 if ( $?PATH == 0 ) then
801   setenv PATH '/foo/bar/bin:/bar/foo/bin'
802 else
803   setenv PATH "$PATH":'/foo/bar/bin:/bar/foo/bin'
804 endif
805
806However, this only works if the code interpreted using the csh
807C<source> command or is included in a csh script inline.  If you
808try to invoke this code using csh C<eval> then it will helpfully
809convert it to one line and if does not work under csh in one line.
810
811=back
812
813There are probably more clever or prettier ways to
814append/prepend path environment variables as I am not a shell
815programmer.  Patches welcome.
816
817Only UNIX (bourne, bash, csh, ksh, fish and their derivatives) and
818Windows (command.com, cmd.exe and PowerShell) are supported so far.
819
820Fish shell support should be considered a tech preview.  The Fish
821shell itself is somewhat in flux, and thus some tests are skipped
822for the Fish shell since behavior is different for different versions.
823In particular, new lines in environment variables may not work on
824newer versions.
825
826Patches welcome for your favorite shell / operating system.
827
828=head1 AUTHOR
829
830Author: Graham Ollis E<lt>plicease@cpan.orgE<gt>
831
832Contributors:
833
834Brad Macpherson (BRAD, brad-mac)
835
836mohawk
837
838=head1 COPYRIGHT AND LICENSE
839
840This software is copyright (c) 2017 by Graham Ollis.
841
842This is free software; you can redistribute it and/or modify it under
843the same terms as the Perl 5 programming language system itself.
844
845=cut
846