Beefy Boxes and Bandwidth Generously Provided by pair Networks
Just another Perl shrine
 
PerlMonks  

Can I from within a module access variables from the calling program?

by HJO (Acolyte)
on Oct 25, 2012 at 11:03 UTC ( [id://1000823]=perlquestion: print w/replies, xml ) Need Help??

HJO has asked for the wisdom of the Perl Monks concerning the following question:

Hi Monks,

I saw earlier in the Chatterbox this question and an answer, and it catched my attention... The answer was "Yes, but it is commonly considered bad practice. You can easily access global variables of the main program: $main::foo gives access to $foo in the main program"

I tried this and obviously it didn't work, so I searched a little and came on this page : Variable Scoping in Perl: the basics, which I saddly didn't understand as good as I would like to :( basically, I understood that there were 3 was to declare a variable, my our and local, after that I kinda lost myself in the subtilities of each method...

So here is my problem : I'm working on a script which uses Log::Log4perl to log pretty much everything I do. I'm calling a homemade module which also needs logging, so I copied/pasted the configuration of Log::Log4perl in it (I don't use an external configuration file for some reasons I'm not going to explain because it would be too long and off topic...) but the name of my .log file is a variable based on the date and time of my system, and I'm afraid that if there's a change in the time (like script launched at 12:12:12 and module called at 12:12:13), I'll end up with two .log files...

So after having though of some horrible methods on my own, this solution to call this variable from the main sript into the module seems perfect... Could someone please help me a little by giving me a very simple explanation on how I need to organize my script/module to make this work ?

I don't know what to give you as additional informations to help me, but here is my configuration of Log::Log4perl, maybe you'll see my problem more clearly ^^" :

my %month = ( 'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06', 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12', ); my $localdate = localtime; my ($dname, $mname, $day, $time, $year) = split( " ",$localdate); my $log_name = "./log/ST03_$year-$month{$mname}-$day\_$time.log"; my $conf = qq{ log4perl.rootLogger = DEBUG, myFILE log4perl.appender.myFILE = Log::Log4perl::App +ender::File log4perl.appender.myFILE.filename = $log_name log4perl.appender.myFILE.mode = append log4perl.appender.myFILE.layout = Log::Log4perl::Lay +out::PatternLayout log4perl.appender.myFILE.layout.ConversionPattern = %d + [%p] (%F line %L) %M %m%n }; Log::Log4perl::init( \$conf ); my $logger = Log::Log4perl->get_logger;

And here if what I'd hope to use in the module :

my $conf = qq{ log4perl.rootLogger = DEBUG, myFILE log4perl.appender.myFILE = Log::Log4perl::App +ender::File log4perl.appender.myFILE.filename = $main::log_name log4perl.appender.myFILE.mode = append log4perl.appender.myFILE.layout = Log::Log4perl::Lay +out::PatternLayout log4perl.appender.myFILE.layout.ConversionPattern = %d + [%p] (%F line %L) %M %m%n }; Log::Log4perl::init( \$conf ); # Charge la configuration my $logger = Log::Log4perl->get_logger;

Thank you, Regards

Replies are listed 'Best First'.
Re: Can I from within a module access variables from the calling program? (modular design)
by tye (Sage) on Oct 25, 2012 at 14:47 UTC

    Wow. Those are some really horrible suggestions for how to solve this problem.

    A well-designed component gets information from the caller by the caller passing in the information to the component. Having components try to reach up into the parent and pull stuff out of the parent is a much worse design choice than even just the using of a global variable.

    You usually pass data into a module either when you 'use' the module or by calling subroutines or methods from the module. For example:

    use Your::Module( $log_file );

    or

    use Your::Module; Your::Module->set_log_file( $log_file );

    The first example leads to some subtle gotchas as well, so just use the second method (just like the designers of Log::Log4perl did):

    package Your::Module; # ... sub set_log_file { my( $we, $log_file ) = @_; Log::Log4perl->init( \"... $log_file ..." ); }

    You don't see Log::Log4perl telling you, "Just set the $log_conf global variable in your package to contain your configuration string and Log::Log4perl will go pull the value from it".

    Having the child module have to know the name of the variable and forcing the parent to make that variable global are both design mistakes. Using PadWalker is particularly crazy. Can you imagine Log::Log4perl saying further "Note that if you declare the variable to be hidden from other modules by using 'my', Log::Log4perl will thwart your stated goal of hiding the variable and will dive into the guts of Perl to find the variable anyway so you might want to add a comment to make this clearer: my $log_conf # Not really private!".

    Worse than the "we'll pass the information by both sides happening to use the identical name" part of this is that you also put a subtle dependency on the order in which things get done. Consider this example:

    package main; use strict; use Log::Log4perl; use Your::Module; our $log_file = ...; Log::Log4perl->init( ... );

    and

    package Your::Module; Log::Log4perl->init( \qq< ... $main::log_file ... > );

    So, after you notice that this isn't working, you go to fix the order in which things are done using a more awkward layout:

    package main; use strict; use Log::Log4perl; our $log_file = ...; Log::Log4perl->init( ... ); use Your::Module; # Must happen after $log_file is set. DON'T MOVE UP!

    But that doesn't work either.

    Pass in the data needed by code when you call the code that needs the data. Then the audience playing at home has a better chance to more quickly understand what is going on.

    But I also thought that Log::Log4perl dislikes being initialized more than once in the same process. So I suspect that your design has even more problems in it than just the passing of the log file name. Indeed, seeing you repeat the identical configuration for Log4perl in two places looks like the sign of poor design to me. It sounds like you noticed that you were computing the name of the log file in two places only because you didn't manage to always come up with the same name in both places.

    Computing the same thing in two places using code that was copy-and-pasted is poor design. Copying and pasting code is always a bad idea (usually a really bad idea). But declaring it unacceptable for the two sides to come up with different answers is even worse.

    Repeating the configuration and initialization of Log::Log4perl looks like a design mistake. Of course, I find the initialization of Log::Log4perl to be terribly awkward. The way one uses Log::Log4perl is moderately awkward, something that I'd probably consider a good choice in most modules but one that I find unfortunate for a logging module.

    I believe that a logging module should go to unusual lengths to make it extra easy to add logging so that more logging is likely to get added before the need for the logging actually comes up. When a problem is found with some code and I need to debug what is going on in a deployable service, then it is often inconvenient to reproduce the problem in a way where I can use the Perl debugger. So being able to just turn up some existing logging and get a lot more details about what is going on is often a huge time saver.

    So Log::Lager tries to make it really easy to add some trace. The module needs further enhancements (some of which are happening now, at least in the git repo, soon to be on CPAN, I hope). So you might also look into using it.

    - tye        

      Waow...

      first of all thank you for giving me the "right" way to do this...

      But even after reading it, I still don't understand everything you're saying... I'll wait tomorrow to read it with my mind rested so I can apply what you're saying ^^"

      Hi tye,

      I finally found some time to try to understand your anwser, I think I did, and most of what you are saying makes sense to me.

      So first, I'm using Log::Log4perl because when I looked at a module that could log things, it was one of the first to pop out, and with sufficient help on the web and good comments on it so I could easily learn to use it. (BTW I checked your Log::Lager, and didn't found it on CPAN as your link indicates, and on google I found it on v 0.04.XXX, so thanks for the alternative, but I will stick on Log::Log4perl a little bit longer...)

      Secondly, yes my design is very poor, because I don't plan very much things when I'm coding, due to the fact that I'm quite a beginner (well not a complete one) on Perl and on developping script in a professionnal environment... So yes, my code is not very beautiful on those aspects, but I'm trying with the best I can do, but of course, it's not always (well I'd even say never) in the way of Perl, or the good practices with module programming...

      So my original issue was : how can I have some coherent logging from my script AND my module (in the same file), without having to double the declaration of my logger ? (of course it would also be perfect if this solution was compatible with Log::Log4perl)

      It seems you're quite knowledgeable on this subject, so I'm asking for help and guidance.

      Thank you very much in advance for your answer.

Re: Can I from within a module access variables from the calling program?
by tobyink (Canon) on Oct 25, 2012 at 12:40 UTC

    Though in your particular case, the module "pulling information" from the main script about the log file name is silly. Instead the main script should "push information" to the module. For example:

    use v5.10; use strict; use warnings; BEGIN { package Some::Module; no thanks; our $LOGFILE; sub what_is_logfile { say "logfile is $LOGFILE"; } }; package main; use Some::Module; my $log = 'foo.txt'; $Some::Module::LOGFILE = $log; Some::Module->what_is_logfile;
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Hi tobyink,

      Well, thanks for your honesty ^^" I understand why you said that I didn't have the most suitable solution for my problem, but I'm a beginner and I also asked here to have other ways to do it (well it was not obvious, I can give that to you ^^")

      anyway I think I'll continue to use my method because I simply didn't understand yours very well

Re: Can I from within a module access variables from the calling program?
by tobyink (Canon) on Oct 25, 2012 at 12:25 UTC
    use v5.10; use strict; use warnings; use PadWalker (); { package Some::Module; sub what_is_x { my $value = $Other::Module::x; say "\$x is $value"; } # Just because PadWalker exists, doesn't mean it's a good idea... sub what_is_y { my $value = ${ PadWalker::peek_my(1)->{'$y'} }; say "\$y is $value"; } } { package Other::Module; our $x = 123; my $y = 456; Some::Module::what_is_x(); Some::Module::what_is_y(); }
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Hi,

      Well thanks for your answer, I didn't understood it at first but now it get it ^^

      I just have two "problems" with your answer, first I'm using Perl 5.8.8 and I'm not sure that your technique is compatible with this version since you're using Perl 5.10

      Secondly, I would have prefered not to use another Module to do so

      but thansk anyway, maybe your answer will help somebody else later

        The only 5.10-specific feature I used was say which is a new (well... five years old) keyword which works like print but automatically appends a new line.

        perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
Re: Can I from within a module access variables from the calling program?
by daxim (Curate) on Oct 25, 2012 at 11:08 UTC

      Thank you for your answer

      It is related in fact but what I'm trying to do is the opposite... They are explaining how to get a variable from a module in the main script, I want to know how to get a variable from the min script into a module...

Re: Can I from within a module access variables from the calling program?
by choroba (Cardinal) on Oct 25, 2012 at 11:52 UTC
    You cannot access my variable from a different package. Declare it with our.
    لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ

      I tried to use our, but it didn't work either

Re: Can I from within a module access variables from the calling program?
by kcott (Archbishop) on Oct 25, 2012 at 12:00 UTC

    G'day HJO,

    "Yes, but it is commonly considered bad practice. You can easily access global variables of the main program: $main::foo gives access to $foo in the main program"

    I tried this and obviously it didn't work, ...

    I don't see how this ties in with your question but what you claim obviously doesn't work, clearly does:

    $ perl -le '$foo = "whatever"; print $main::foo;' whatever

    -- Ken

      Subtle difference:

      perl -le 'my $foo = "whatever"; print $main::foo;'

      --MidLifeXis

        I may be missing the point you're making but HJO's statement referred to global variables. Other examples with variables explicitly declared as global:

        $ perl -le 'local $foo = "whatever"; print $main::foo;' whatever
        $ perl -le 'our $foo = "whatever"; print $main::foo;' whatever

        -- Ken

      Hi,thank you for answering.

      Well, the obvious part is that I was asking help here so I didn't manage to make this working.

      Could you please look at the code I gave, when I try to run the script, it says to me that there's an "Use of uninitialized value in concatenation (.) or string at..." in my module... so here is why I'm asking some help on the matter

        Use of uninitialized value
        Oh, I see. You were probably not following the whole conversation in the ChatterBox. If you are not mentioning the variable in a subroutine in the package, the code is executed before you assign any value to the variable in main::. You have to assign the value in a BEGIN block in order to be accessible from within the top level of the package.
        لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ

        Hi HJO,

        Try this, In your module declare global variable, then export variable

        .... our %glo_var = ('a'=>100,'b'=>200); ... @EXPORT = qw(%glo_var ... members); ... 1;

        In your tool

        use SampleModule::TestMod; print $glo_var{'a'};exit;

        Output=100

Re: Can I from within a module access variables from the calling program?
by Anonymous Monk on Oct 29, 2012 at 09:17 UTC

      Also heed the security warning in the FAQ item I linked above, and circumvent the security issue with a custom appender like (the untested):

      package Log::Log4perl::Appender::HJOfilnamed; use parent qw[ Log::Log4perl::Appender::File ]; use POSIX(); sub new { my $class = shift; my %opts = @_; my $tim = POSIX::strftime('%Y-%m-%dT%H-%M-%SZ', gmtime); my $fname = join '.', grep defined, $opts{name}, $opts{appender}, +$tim, 'log'; $fname =~ s{::}{.}g; $fname =~ s{[^0-9a-zA-Z\-\+\.}{}g; $fname = $1 if $fname =~ /^(.*)$/ return $class->new( @_, filename => $fname ); }
      So  log4perl.appender.myFILE = Log::Log4perl::Appender::HJOfilnamed gets you a  MyApp.myFILE.2012-10-29T02-40-04Z.log

      You'd probably want to set other defaults to your liking :)( layout, mode, ... )

        Hi Anonymous Monk,

        Thank you for your answer, it seems to fix my issue, but I'm afraid I don't understand it quite well :(

        package Log::Log4perl::Appender::HJOfilnamed; use parent qw[ Log::Log4perl::Appender::File ];

        Are you creating and entire module juste for the purpose of logging ? I mean, that's a good idea, I must admit it never accured to me that I could do so... And the use parent, what is it doing precisely please ?

        If I understand quite well, you are creating an object to create a new name at every launch of the main script ? And you are calling it at the intialization of the logger ? Does this allows me to have the exact same logfile name for my script and its module ? (I mean, even if there's a delay of about a second between the two initializatons)

        And thanks for the way you are naming your file, basically I use the same, but I'll stick to my method, it seems rather complicated the way you do it, at least to complicate for me...

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://1000823]
Approved by Ratazong
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others scrutinizing the Monastery: (4)
As of 2024-03-28 23:12 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found