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