[perl] 모듈에서 정의되었지만 런타임 단계에서 사용되기 전에 함수를 덮어 쓰시겠습니까?

아주 간단한 것을 봅시다.

# Foo.pm
package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}

어쨌든 설정된 test.pl것을 변경 하고 화면에 다른 것을 인쇄 하는 실행 코드를 사용할 수 있습니까?$bazFoo.pm

# maybe something here.
use Foo;
# maybe something here

컴파일러 단계에서 위의 내용을 강제로 인쇄 할 수 7있습니까?



답변

해킹 때문에 필요하다 require(및 use) 모두 컴파일 실행 모듈을 반환하기 전에.

동일합니다 eval. eval코드를 실행하지 않고 컴파일하는 데 사용할 수 없습니다.

내가 찾은 가장 방해가 적은 솔루션은 재정의하는 것 DB::postponed입니다. 컴파일 된 필수 파일을 평가하기 전에 호출됩니다. 불행히도 디버깅 할 때만 호출됩니다 ( perl -d).

다른 해결책은 다음과 같이 파일을 읽고 수정하고 수정 된 파일을 평가하는 것입니다.

use File::Slurper qw( read_binary );

eval(read_binary("Foo.pm") . <<'__EOS__')  or die $@;
package Foo {
   no warnings qw( redefine );
   sub bar { 7 }
}
__EOS__

위의 내용은 올바르게 설정되지 않았 %INC으며 경고에 사용되는 파일 이름을 엉망으로 만들고 호출하지 않습니다 DB::postponed. 다음은 더 강력한 솔루션입니다.

use IO::Unread  qw( unread );
use Path::Class qw( dir );

BEGIN {     
   my $preamble = '
      UNITCHECK {
         no warnings qw( redefine );
         *Foo::bar = sub { 7 };
      }
   ';    

   my @libs = @INC;
   unshift @INC, sub {
      my (undef, $fn) = @_;
      return undef if $_[1] ne 'Foo.pm';

      for my $qfn (map dir($_)->file($fn), @libs) {
         open(my $fh, '<', $qfn)
            or do {
               next if $!{ENOENT};
               die $!;
            };

         unread $fh, "$preamble\n#line 1 $qfn\n";
         return $fh;
      }

      return undef;
   };
}

use Foo;

내가 사용하는 UNITCHECK내가 (사용하여 재정의를 앞에 추가하기 때문에 (실행 컴파일 후하지만 전에 호출되는) unread대신에 전체 파일을 읽고 새로운 정의를 추가하는 것보다). 해당 접근 방식을 사용하려면 파일 핸들을 사용하여 반환 할 수 있습니다

open(my $fh_for_perl, '<', \$modified_code);
return $fh_for_perl;

@INC후크 를 언급하기 위해 @Grinnz에게 찬사를 보냅니다 .


답변

여기서 유일한 옵션은 매우 해킹이 될 것이기 때문에, 우리가 정말로 원하는 것은 서브 루틴이 %Foo::숨김 에 추가 된 후 코드를 실행하는 것입니다 .

use strict;
use warnings;

# bless a coderef and run it on destruction
package RunOnDestruct {
  sub new { my $class = shift; bless shift, $class }
  sub DESTROY { my $self = shift; $self->() }
}

use Variable::Magic 0.58 qw(wizard cast dispell);
use Scalar::Util 'weaken';
BEGIN {
  my $wiz;
  $wiz = wizard(store => sub {
    return undef unless $_[2] eq 'bar';
    dispell %Foo::, $wiz; # avoid infinite recursion
    # Variable::Magic will destroy returned object *after* the store
    return RunOnDestruct->new(sub { no warnings 'redefine'; *Foo::bar = sub { 7 } }); 
  });
  cast %Foo::, $wiz;
  weaken $wiz; # avoid memory leak from self-reference
}

use lib::relative '.';
use Foo;


답변

경고가 표시되지만 7을 인쇄합니다.

sub Foo::bar {}
BEGIN {
    $SIG{__WARN__} = sub {
        *Foo::bar = sub { 7 };
    };
}

먼저을 정의 Foo::bar합니다. 이 값은 Foo.pm의 선언에 의해 재정의되지만 “서브 루틴 Foo :: bar 재정의 됨”경고가 트리거되며, 서브 루틴을 다시 재정의하는 신호 처리기를 호출하여 7을 반환합니다.


답변

다음은 모듈 로딩 프로세스 후킹을 Readonly 모듈의 읽기 전용 기능과 결합한 솔루션입니다.

$ cat Foo.pm 
package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}


$ cat test.pl 
#!/usr/bin/perl

use strict;
use warnings;

use lib qw(.);

use Path::Tiny;
use Readonly;

BEGIN {
    my @remap = (
        '$Foo::{bar} => \&mybar'
    );

    my $pre = join ' ', map "Readonly::Scalar $_;", @remap;

    my @inc = @INC;

    unshift @INC, sub {
        return undef if $_[1] ne 'Foo.pm';

        my ($pm) = grep { $_->is_file && -r } map { path $_, $_[1] } @inc
           or return undef;

        open my $fh, '<', \($pre. "#line 1 $pm\n". $pm->slurp_raw);
        return $fh;
    };
}


sub mybar { 5 }

use Foo;


$ ./test.pl   
5


답변

m-conrad의 답변Readonly.pm 에 따라 매우 간단한 대안을 놓쳤다는 것을 알게 된 후에 더 이상 의지하지 않기 위해 여기에서 솔루션 을 수정했습니다.

Foo.pm ( 오픈 포스트와 동일 )

package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}
# Note, even though print normally returns true, a final line of 1; is recommended.

OverrideSubs.pm 업데이트

package OverrideSubs;

use strict;
use warnings;

use Path::Tiny;
use List::Util qw(first);

sub import {
    my (undef, %overrides) = @_;
    my $default_pkg = caller; # Default namespace when unspecified.

    my %remap;

    for my $what (keys %overrides) {
        ( my $with = $overrides{$what} ) =~ s/^([^:]+)$/${default_pkg}::$1/;

        my $what_pkg  = $what =~ /^(.*)\:\:/ ? $1 : $default_pkg;
        my $what_file = ( join '/', split /\:\:/, $what_pkg ). '.pm';

        push @{ $remap{$what_file} }, "*$what = *$with";
    }

    my @inc = grep !ref, @INC; # Filter out any existing hooks; strings only.

    unshift @INC, sub {
        my $remap = $remap{ $_[1] } or return undef;
        my $pre = join ';', @$remap;

        my $pm = first { $_->is_file && -r } map { path $_, $_[1] } @inc
            or return undef;

        # Prepend code to override subroutine(s) and reset line numbering.
        open my $fh, '<', \( $pre. ";\n#line 1 $pm\n". $pm->slurp_raw );
        return $fh;
   };
}

1;

test-run.pl

#!/usr/bin/env perl

use strict;
use warnings;

use lib qw(.); # Needed for newer Perls that typically exclude . from @INC by default.

use OverrideSubs
    'Foo::bar' => 'mybar';

sub mybar { 5 } # This can appear before or after 'use OverrideSubs', 
                # but must appear before 'use Foo'.

use Foo;

실행 및 출력 :

$ ./test-run.pl 
5


답변

경우 sub bar내부가 Foo.pm기존과는 다른 프로토 타입이 Foo::bar기능을 펄을 덮어 쓰지 않습니다? 그것은 사실 인 것처럼 보이고 솔루션을 매우 간단하게 만듭니다.

# test.pl
BEGIN { *Foo::bar = sub () { 7 } }
use Foo;

또는 같은 것

# test.pl
package Foo { use constant bar => 7 };
use Foo;

업데이트 : 아니요, 이것이 작동하는 이유는 Perl이 “일정한”서브 루틴을 정의하지 않기 ()때문에 (프로토 타입 ), 모의 함수가 일정하면 실행 가능한 솔루션 일뿐입니다.


답변

골프 콘테스트를하자!

sub _override { 7 }
BEGIN {
  my ($pm)= grep -f, map "$_/Foo.pm", @INC or die "Foo.pm not found";
  open my $fh, "<", $pm or die;
  local $/= undef;
  eval "*Foo::bar= *main::_override;\n#line 1 $pm\n".<$fh> or die $@;
  $INC{'Foo.pm'}= $pm;
}
use Foo;

이것은 단지 모듈의 코드 앞에 메소드를 대체하는 것으로, 컴파일 단계 이후와 실행 단계 전에 실행되는 첫 번째 코드 행이됩니다.

그런 다음, %INC장래의로드가 use Foo원본을 끌어 오지 않도록 항목을 채우십시오 .