diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml deleted file mode 100644 index e520750..0000000 --- a/.github/workflows/cygwin.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: cygwin - -on: - push: - branches: - - '*' - tags-ignore: - - '*' - pull_request: - -env: - PERL5LIB: /cygdrive/c/cx/lib/perl5 - PERL_LOCAL_LIB_ROOT: /cygdrive/cx - PERL_MB_OPT: --install_base /cygdrive/c/cx - PERL_MM_OPT: INSTALL_BASE=/cygdrive/c/cx - ALIEN_BUILD_PLUGIN_PKGCONFIG_COMMANDLINE_TEST: 1 # Test Alien::Build::Plugin::PkgConfig::CommandLine - CYGWIN_NOWINPATH: 1 - -jobs: - perl: - - runs-on: windows-latest - - strategy: - fail-fast: false - - defaults: - run: - shell: C:\tools\cygwin\bin\bash.exe --login --norc -eo pipefail -o igncr '{0}' - - steps: - - name: Set git to use LF - run: | - git config --global core.autocrlf false - git config --global core.eol lf - shell: powershell - - - uses: actions/checkout@v2 - - - name: Set up Cygwin - uses: egor-tensin/setup-cygwin@v3 - with: - platform: x64 - packages: make perl gcc-core gcc-g++ pkg-config libcrypt-devel libssl-devel git libffi-devel libarchive-devel - - - name: perl -V - run: | - perl -V - gcc --version - - - name: Prepare for cache - run: | - perl -V > perlversion.txt - gcc --version >> perlversion.txt - ls perlversion.txt - - - name: Cache CPAN modules - uses: actions/cache@v1 - with: - path: c:\cx - key: ${{ runner.os }}-build-cygwin-${{ hashFiles('perlversion.txt') }} - restore-keys: | - ${{ runner.os }}-build-cygwin-${{ hashFiles('perlversion.txt') }} - - - name: Install Static Dependencies - run: | - export PATH="/cygdrive/c/cx/bin:$PATH" - cd $( cygpath -u $GITHUB_WORKSPACE ) - yes | cpan App::cpanminus || true - cpanm -n Dist::Zilla - perl -S dzil authordeps --missing | perl -S cpanm -n - perl -S dzil listdeps --missing | perl -S cpanm -n - - - name: Install Dynamic Dependencies - run: | - export PATH="/cygdrive/c/cx/bin:$PATH" - cd $( cygpath -u $GITHUB_WORKSPACE ) - perl -S dzil run --no-build 'perl -S cpanm --installdeps .' - - - name: Run Tests - run: | - export PATH="/cygdrive/c/cx/bin:$PATH" - cd $( cygpath -u $GITHUB_WORKSPACE ) - perl -S dzil test -v - - - name: CPAN log - if: ${{ failure() }} - run: | - cat ~/.cpanm/latest-build/build.log diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 825b365..edff2b7 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -17,8 +17,11 @@ jobs: fail-fast: false matrix: cip_tag: - - static - - "5.37" + - "5.45" + - "5.44" + - "5.42" + - "5.40" + - "5.38" - "5.36" - "5.34" - "5.32" @@ -32,14 +35,12 @@ jobs: - "5.16" - "5.14" - "5.12" - - "5.10" - - "5.8" env: CIP_TAG: ${{ matrix.cip_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Bootstrap CIP run: | @@ -52,7 +53,7 @@ jobs: cip cache-key - name: Cache CPAN modules - uses: actions/cache@v2 + uses: actions/cache@v6 with: path: ~/.cip key: ${{ runner.os }}-build-${{ steps.cache-key.outputs.key }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d2b8134..36b6276 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Set up Perl run: | @@ -40,7 +40,7 @@ jobs: ls -l perlversion.txt - name: Cache CPAN modules - uses: actions/cache@v1 + uses: actions/cache@v6 with: path: ~/perl5 key: ${{ runner.os }}-build-${{ hashFiles('perlversion.txt') }} diff --git a/.github/workflows/msys2-mingw.yml b/.github/workflows/msys2-mingw.yml deleted file mode 100644 index 2743442..0000000 --- a/.github/workflows/msys2-mingw.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: msys2-mingw - -on: - push: - branches: - - '*' - tags-ignore: - - '*' - pull_request: - -env: - PERL5LIB: /c/cx/lib/perl5:/c/cx/lib/perl5/MSWin32-x64-multi-thread - PERL_LOCAL_LIB_ROOT: c:/cx - PERL_MB_OPT: --install_base C:/cx - PERL_MM_OPT: INSTALL_BASE=C:/cx - ALIEN_BUILD_PLUGIN_PKGCONFIG_COMMANDLINE_TEST: 1 # Test Alien::Build::Plugin::PkgConfig::CommandLine - -jobs: - perl: - - runs-on: windows-latest - - strategy: - fail-fast: false - - defaults: - run: - shell: msys2 {0} - - steps: - - name: Set git to use LF - run: | - git config --global core.autocrlf false - git config --global core.eol lf - shell: powershell - - - uses: actions/checkout@v2 - - - name: Set up Perl - uses: msys2/setup-msys2@v2 - with: - update: true - install: >- - base-devel - mingw-w64-x86_64-toolchain - mingw-w64-x86_64-perl - mingw-w64-x86_64-libffi - mingw-w64-x86_64-libarchive - - - name: perl -V - run: | - perl -V - - - name: Prepare for cache - run: | - perl -V > perlversion.txt - ls perlversion.txt - - - name: Cache CPAN modules - uses: actions/cache@v1 - with: - path: c:\cx - key: ${{ runner.os }}-build-msys2-${{ hashFiles('perlversion.txt') }} - restore-keys: | - ${{ runner.os }}-build-msys2-${{ hashFiles('perlversion.txt') }} - - - name: Install Static Dependencies - run: | - export PATH="/c/cx/bin:$PATH" - yes | cpan App::cpanminus || true - cpanm -n Dist::Zilla - perl -S dzil authordeps --missing | perl -S cpanm -n - perl -S dzil listdeps --missing | perl -S cpanm -n - - - name: Install Dynamic Dependencies - run: | - export PATH="/c/cx/bin:$PATH" - perl -S dzil run --no-build 'perl -S cpanm --installdeps .' - - - name: Run Tests - run: | - export PATH="/c/cx/bin:$PATH" - perl -S dzil test -v diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..cba7de1 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,28 @@ +name: static + +on: + push: + branches: + - '*' + tags-ignore: + - '*' + pull_request: + +jobs: + perl: + + runs-on: ubuntu-latest + + env: + CIP_TAG: static + + steps: + - uses: actions/checkout@v2 + + - name: Bootstrap CIP + run: | + curl -L https://raw.githubusercontent.com/uperl/cip/main/bin/github-bootstrap | bash + + - name: Build + Test + run: | + cip script diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 913e2d0..16cbd07 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -28,7 +28,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Set up Perl run: | @@ -43,7 +43,7 @@ jobs: perl -V > perlversion.txt - name: Cache CPAN modules - uses: actions/cache@v1 + uses: actions/cache@v6 env: cache-name: cache-cpan-modules with: diff --git a/Changes b/Changes index e2729f7..5e55f83 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,9 @@ Revision history for {{$dist->name}} {{$NEXT}} + - Add unset method to remove environment variables (gh#14, gh#27, gh#30) + - set_alias now accepts an array reference of words, quoting each + one individually so embedded spaces are preserved (gh#17, gh#30) 0.34 2020-02-05 10:20:27 -0700 - Better diagnostics in testing (gh#22) diff --git a/README.md b/README.md index 8131155..ccad42a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Shell::Config::Generate ![linux](https://github.com/plicease/Shell-Config-Generate/workflows/linux/badge.svg) ![macos](https://github.com/plicease/Shell-Config-Generate/workflows/macos/badge.svg) ![windows](https://github.com/plicease/Shell-Config-Generate/workflows/windows/badge.svg) ![cygwin](https://github.com/plicease/Shell-Config-Generate/workflows/cygwin/badge.svg) ![msys2-mingw](https://github.com/plicease/Shell-Config-Generate/workflows/msys2-mingw/badge.svg) +# Shell::Config::Generate ![linux](https://github.com/plicease/Shell-Config-Generate/workflows/linux/badge.svg) ![macos](https://github.com/plicease/Shell-Config-Generate/workflows/macos/badge.svg) ![windows](https://github.com/plicease/Shell-Config-Generate/workflows/windows/badge.svg) Portably generate config for any shell @@ -285,14 +285,39 @@ $config->echo_on; Turn off the echo off (that is do not put anything at the beginning of the config) for DOS/Windows configurations (`command.com` or `cmd.exe`). +## unset + +``` +$config->unset( $name ); +``` + +Unset (remove) an environment variable. + ## set\_alias ```perl $config->set_alias( $alias => $command ) +$config->set_alias( $alias => \@command ) ``` Sets the given alias to the given command. +`$command` may also be given as an array reference of words +(a command name followed by its arguments). This works just +like the plain string form, except that each word is quoted +individually, so any spaces embedded in a word will be preserved +as part of that word instead of being treated as a word separator. + +**note** that `csh` and `tcsh` aliases work by splicing the +alias text back into the command line and splitting it again on +whitespace, with no surviving quoting mechanism, so embedded +spaces in a word cannot be protected on those shells even when +`$command` is given as an array reference. Rather than silently +generating an alias that will not work as expected, `generate` +and `generate_file` will throw an exception with a helpful +message if any word contains a space and the target shell is +`csh` or `tcsh`. + Caveat: some older shells do not support aliases, such as the original bourne shell. This module will generate @@ -462,7 +487,7 @@ mohawk # COPYRIGHT AND LICENSE -This software is copyright (c) 2017 by Graham Ollis. +This software is copyright (c) 2017-2026 by Graham Ollis. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. diff --git a/dist.ini b/dist.ini index ad25ec7..3d68192 100644 --- a/dist.ini +++ b/dist.ini @@ -2,20 +2,18 @@ name = Shell-Config-Generate author = Graham Ollis license = Perl_5 copyright_holder = Graham Ollis -copyright_year = 2017-2022 +copyright_year = 2017-2026 version = 0.34 [@Author::Plicease] -:version = 2.69 +:version = 2.79 release_tests = 1 test2_v0 = 1 workflow = linux workflow = macos workflow = windows -workflow = cygwin -workflow = msys2-mingw [Author::Plicease::Core] diff --git a/lib/Shell/Config/Generate.pm b/lib/Shell/Config/Generate.pm index 399c7a9..f7f6826 100644 --- a/lib/Shell/Config/Generate.pm +++ b/lib/Shell/Config/Generate.pm @@ -400,12 +400,74 @@ sub _value_escape_powershell $value; } +sub _quote_word_sh +{ + "'" . _value_escape_sh(shift) . "'"; +} + +sub _quote_word_csh +{ + "'" . _value_escape_csh(shift) . "'"; +} + +sub _quote_word_fish +{ + "'" . _value_escape_fish(shift) . "'"; +} + +sub _quote_word_win32 +{ + my $value = _value_escape_win32(shift); + $value =~ /\s/ ? qq{"$value"} : $value; +} + +sub _quote_word_powershell +{ + my $value = shift() . ''; + $value =~ s/(')/''/g; + "'$value'"; +} + +=head2 unset + + $config->unset( $name ); + +Unset (remove) an environment variable. + +=cut + +sub unset +{ + my($self, $name) = @_; + + push @{ $self->{commands} }, ['unset', $name]; + + $self; +} + =head2 set_alias $config->set_alias( $alias => $command ) + $config->set_alias( $alias => \@command ) Sets the given alias to the given command. +C<$command> may also be given as an array reference of words +(a command name followed by its arguments). This works just +like the plain string form, except that each word is quoted +individually, so any spaces embedded in a word will be preserved +as part of that word instead of being treated as a word separator. + +B that C and C aliases work by splicing the +alias text back into the command line and splitting it again on +whitespace, with no surviving quoting mechanism, so embedded +spaces in a word cannot be protected on those shells even when +C<$command> is given as an array reference. Rather than silently +generating an alias that will not work as expected, C +and C will throw an exception with a helpful +message if any word contains a space and the target shell is +C or C. + Caveat: some older shells do not support aliases, such as the original bourne shell. This module will generate @@ -626,6 +688,35 @@ sub _generate } } + elsif($command eq 'unset') + { + my($name) = @$args; + if($shell->is_c) + { + $buffer .= "unsetenv $name;\n"; + } + elsif($shell->is_fish) + { + $buffer .= "set -e $name;\n"; + } + elsif($shell->is_bourne) + { + $buffer .= "unset $name;\n"; + } + elsif($shell->is_cmd || $shell->is_command) + { + $buffer .= "set $name=\n"; + } + elsif($shell->is_power) + { + $buffer .= "Remove-Item -Path Env:\\$name -ErrorAction SilentlyContinue\n"; + } + else + { + croak 'don\'t know how to "unset" with ' . $shell->name; + } + } + elsif($command eq 'comment') { if($shell->is_unix || $shell->is_power) @@ -644,25 +735,62 @@ sub _generate elsif($command eq 'alias') { + my($alias, $cmd) = @$args; + my @words = ref($cmd) eq 'ARRAY' ? @$cmd : (); + if($shell->is_bourne) { - $buffer .= "alias $args->[0]=\"$args->[1]\";\n"; + my $value = @words ? join(' ', map { _quote_word_sh($_) } @words) : $cmd; + $buffer .= "alias $alias=\"$value\";\n"; } elsif($shell->is_c) { - $buffer .= "alias $args->[0] $args->[1];\n"; + my $value; + if(@words) + { + for(@words) + { + croak "cannot generate alias '$alias' for " . $shell->name + . ": word '$_' contains a space, which is not supported" + . " for alias commands on csh/tcsh" + if /\s/; + } + $value = join ' ', map { _quote_word_csh($_) } @words; + } + else + { + $value = $cmd; + } + $buffer .= "alias $alias $value;\n"; } elsif($shell->is_cmd || $shell->is_command) { - $buffer .= "DOSKEY $args->[0]=$args->[1] \$*\n"; + my $value = @words ? join(' ', map { _quote_word_win32($_) } @words) : $cmd; + $buffer .= "DOSKEY $alias=$value \$*\n"; } elsif($shell->is_power) { - $buffer .= sprintf("function %s { %s \$args }\n", $args->[0], _value_escape_powershell($args->[1])); + if(@words) + { + my $value = join ' ', map { _quote_word_powershell($_) } @words; + $buffer .= sprintf("function %s { & %s \$args }\n", $alias, $value); + } + else + { + $buffer .= sprintf("function %s { %s \$args }\n", $alias, _value_escape_powershell($cmd)); + } } elsif($shell->is_fish) { - $buffer .= "alias $args->[0] '$args->[1]';\n"; + if(@words) + { + my $value = join ' ', map { _quote_word_fish($_) } @words; + $buffer .= "alias $alias \"$value\";\n"; + } + else + { + $buffer .= "alias $alias '$cmd';\n"; + } } else { diff --git a/t/shell_config_generate__alias_array.t b/t/shell_config_generate__alias_array.t new file mode 100644 index 0000000..5cc5891 --- /dev/null +++ b/t/shell_config_generate__alias_array.t @@ -0,0 +1,94 @@ +use lib 't/lib'; +use Test2::V0 -no_srand => 1; +use File::Spec; +use Shell::Guess; +use Shell::Config::Generate; +use TestLib; + +my $dir = tempdir(); + +my $perl_exe = $^X; +$perl_exe = Win32::GetShortPathName($perl_exe) if $^O eq 'MSWin32'; + +my $config = eval { Shell::Config::Generate->new }; +isa_ok $config, 'Shell::Config::Generate'; + +my $script_name = File::Spec->catfile($dir, 'fooecho.pl'); +do { + open my $fh, '>', $script_name; + print $fh join("\n", 'use strict;', + 'use warnings;', + 'use Data::Dumper;', + 'print Dumper(\@ARGV);', + '', + ); + close $fh; +}; + +eval { $config->set_alias("myecho2", [ $perl_exe, $script_name, 'f00f', 'has space' ]) }; +is $@, '', 'set_alias'; + +foreach my $shell (qw( tcsh csh bsd-csh bash sh zsh cmd.exe command.com ksh 44bsd-csh jsh powershell.exe pwsh fish )) +{ + subtest $shell => sub { + skip_all 'jsh does not have aliases' if $shell eq 'jsh'; + my $shell_path = find_shell($shell); + my $guess = TestLib::get_guess($shell); + + if($shell =~ /csh$/) + { + # csh/tcsh aliases re-tokenize the alias text on whitespace with no + # surviving quoting mechanism, so an embedded space cannot be + # protected on those shells. generate should refuse to produce + # a broken alias and die with a helpful message instead. + my $ret = eval { $config->generate($guess) }; + like $@, qr/word 'has space' contains a space/, 'generate dies with a helpful message'; + is $ret, U(), 'generate did not return a value'; + return; + } + + note $config->generate($guess); + skip_all "no $shell found" unless defined $shell_path; + skip_all "not testing sh in case it doesn't support aliases" if $shell eq 'sh'; + skip_all "alias may not work with non-interactive cmd.exe or command.com" + if $shell eq 'cmd.exe' || $shell eq 'command.com'; + skip_all "skipping powershell on msys" + if $shell eq 'powershell.exe' && $^O =~ /^(msys)$/; + my $list = get_env($config, $shell, $shell_path, 'myecho2 one two three'); + return unless defined $list; + + is $list, [ 'f00f', 'has space', qw( one two three )], 'arguments match, space preserved'; + }; +} + +subtest 'powershell.exe' => sub { + my $shell = 'powershell.exe'; + $shell = 'pwsh' unless $^O =~ /^(MSWin32|cygwin|msys)$/; + my $shell_path = find_shell($shell); + my $guess = TestLib::get_guess($shell); + + if($^O eq 'cygwin') + { + $config = Shell::Config::Generate->new; + $config->set_alias("myecho2", [ map { Cygwin::posix_to_win_path($_) } $perl_exe, $script_name, 'f00f', 'has space' ]); + } + + note $config->generate($guess); + skip_all "no powershell.exe found" unless defined $shell_path; + + my $list = get_env($config, $shell, $shell_path, 'myecho2 one two three'); + return unless defined $list; + is $list, [ 'f00f', 'has space', qw( one two three )], 'arguments match, space preserved'; +}; + +subtest 'csh/tcsh die without spaces in words' => sub { + # sanity check: an alias without any embedded spaces in its words + # should generate fine for csh/tcsh. + my $config2 = Shell::Config::Generate->new; + $config2->set_alias("myecho3", [ 'ls', '-CF' ]); + my $ret = eval { $config2->generate(Shell::Guess->c_shell) }; + is $@, '', 'no exception'; + like $ret, qr/^alias myecho3 'ls' '-CF';$/m, 'generated alias looks right'; +}; + +done_testing; diff --git a/t/shell_config_generate__unset.t b/t/shell_config_generate__unset.t new file mode 100644 index 0000000..10d4421 --- /dev/null +++ b/t/shell_config_generate__unset.t @@ -0,0 +1,42 @@ +use lib 't/lib'; +use Test2::V0 -no_srand => 1; +use Shell::Config::Generate; +use TestLib; + +tempdir(); + +my $config = eval { Shell::Config::Generate->new }; + +isa_ok $config, 'Shell::Config::Generate'; + +$config->set( FOO_KEEP => 'bar' ); +$config->set( FOO_UNSET => 'baz' ); + +my $ret = eval { $config->unset( 'FOO_UNSET' ) }; +diag $@ if $@; +isa_ok $ret, 'Shell::Config::Generate'; + +foreach my $shell (qw( tcsh csh bsd-csh bash sh zsh cmd.exe command.com ksh 44bsd-csh jsh powershell.exe pwsh fish )) +{ + subtest $shell => sub { + my $shell_path = find_shell($shell); + skip_all "$shell not found" unless defined $shell_path; + + my $env = get_env($config, $shell, $shell_path); + return unless defined $env; + + is + $env, + hash { + field FOO_KEEP => 'bar'; + etc; + }, + $shell, + ; + + ok !exists $env->{FOO_UNSET}, 'FOO_UNSET is not set'; + + } +} + +done_testing;