Eval im Kontext der aufrufenden Funktion?

Begonnen von StefanStrobel, 23 Juni 2020, 13:07:07

Vorheriges Thema - Nächstes Thema

StefanStrobel

Hallo,

ich versuche gerade HTTPMOD zu modernisieren, in ein eigenes Paket zu packen und dabei einige Funktionen, die in HTTPMOD, Modbus und anderen Modulen redundant vorkommen, in ein neues Paket auszulagern.
Beide Module bieten ja an einigen Stellen die Möglichkeit Perl-Expressions als Attribut zu definieren, so dass man Ein- und Ausgaben flexibel umarbeiten kann.
Wenn ich das nun zentral in einer Funktion (z.B. ModifyWithExpr) bündeln will, habe ich die Herausforderung, dass bisher diverse Variable in der Expression zur Verfügung stehen. Sobald ich den Eval in eine zentrale Funktion schiebe, sind die aber nicht mehr sichtbar und ich müsste alle potentiell benötigten Variablen beim Aufruf übergeben und innerhalb von ModifyWithExpr mit entsprechendem Namen setzen (siehe $old, $val, @setValArr).

Gibt es in Perl so etwas wie uplevel (in TCL), mit dem Code in einem übergeordneten Scope ausgeführt wird?
Ich könnte natürlich auch einen Hash mit Variablennamen und Werte-Referenzen übergeben und daraus dann in einer Schleife lokalen Variablen erzeugen, aber so richtig schön erscheint mir das auch nicht.

Hat jemand eine Idee, wie man das elegant lösen kann?

hier ein erster Entwurf zur Verdeutlichung:

sub ModifyWithExpr {
    my $hash   = shift;                     # the current device hash
    my $action = shift;                     # text to describe the current context for logging
    my $exp    = shift;                     # the expression to be used
    my $text   = shift;                     # input text to be avalibale as $val and $old
    my $aRef   = shift;                     # optional setValArr as reference for use in expressions
    my $name   = $hash->{NAME};

    if ($exp) {
        my $old       = $text;              # if used for IExpr
        my $val       = $text;              # if used for IExpr
        my @setValArr = @{$aRef};           # if used for IExpr
        local $SIG{__WARN__} = sub { Log3 $name, 3, "$name: $action with expresion $exp on $text created warning: @_"; };
        $text = eval $exp;                  ## no critic
        if ($@) {
            Log3 $name, 3, "$name: $action with expression $exp on $text created error: $@";
        }
        Log3 $name, 5, "$name: $action converted\n$old\nto\n$text\nusing expr $exp";
    }   
    return $text;
}


Gruss
   Stefan

justme1968

die variablen explizit zu übergeben statt sich auf den scope zu verlassen hat aber den vorteil das das verhalten besser definiert ist und man könnte vermutlich auch einfacher testen.

prinzipiell gibt es in fhem ja die EvalSpecials routine die dafür da ist. ganz nebenbei hätte das zugehörige AnalyzeCommandChain dann auch noch den vorteil das man das man nicht nur perl sondern auch fhem kommanandos nutzen könnte. wobei ich nicht weiss ob letzteres hier relevant ist. auch perlSyntaxCheck unterstützt das gleiche specials format und damit kann man dann den anwender gegebenenfalls gleich bei der eingabe einer routine auf probleme hinweisen.

auch die allowed und asyncOutput funktionalität wäre hier integriert.

auch wenn vielleicht nicht die 'perfekte' überarbeitung ist wäre bestehende funktionalität schön wiederverwendet und in einem späteren schritt könnte man dann alles zusammen verbessern wenn nötig.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

StefanStrobel

Vielen Dank für den Hinweis.
AnalyzeCommandChain, EvalSpecials etc. wäre schön gewesen, wenn ich das von Anfang an gemacht hätte.
Wenn ich es jetzt umstelle, werden aber Expressions wie "$val / 10" nicht mehr funktionieren, da $val nicht bekannt ist.
Oder übersehe ich da was?
Vermutlich muss ich doch ähnlich wie bei EvalSpecials einen Hash mit Variablennamen und Werten an eine neue Funktion übergeben.
Ich könnte auch per Attribut von der alten Eval-Technik auf EvalSpecials umstellen lassen und darum bitten, die Expressions so anzupassen, dass sie damit funktionieren bzw. die Werte auch über $hash->{'SetVal'} o.ä. bereitstellen...
Ich werde mal ein paar Sachen ausprobieren.

Gruss / vielen Dank
    Stefan

rudolfkoenig

Man kann EvalSpecials beliebige Variablen uebergeben (mit dem etwas merkwuerdigen Syntax %NAME=>Wert), diese stehen dann als $NAME dem AnalyzePerlCommand zur Verfuegung. Siehe auch 10_MQTT2_DEVICE.pm, ab Zeile 165.

Alle evals, die Benutzer aendern koennen, muessen AnalyzePerlCommand verwenden, sonst ist allowed wirkungslos.

StefanStrobel

Ok, das ist genau das was ich gesucht habe :-)
Dann baue ich das so in HTTPMOD und meine anderen Module ein.

Gruss und vielen Dank!
    Stefan


StefanStrobel

Ich fürchte ich habe mich doch zu früh gefreut.

Für 98_Modbus.pm muss ich dem Eval eine Array-Variable bereitstellen. Bisher scheinen nur Scalars als EvalSpecials vorgesehen zu sein.
Zudem bräuchte ich auch noch eine Variante, mit der ich nur die Syntax der Expression prüfen kann (für HTTPMOD_Attr etc.).

Gruss
   Stefan

rudolfkoenig

Fuer die Syntax gibts perlSyntaxCheck().
Fuer Arrays habe ich aber keine Loesung, evtl. ist es ein Workaround diese ueber $data{XXX} anzubieten.

StefanStrobel

Hallo Rudi,

Für die Arrays hätte ich noch eine Idee:

Da EvalSpecials ja einen String mit mehreren "my varxy" erzeugt, der dann von AnalyzePerlCommand vor den eigentlichen Perl-String gehängt wird, ist es schwer etwas komplexeres als Scalars auf diesem Weg zu übergeben.
Auch mit Referenzen auf Arrays oder Hashes klappt es so offenbar nicht.
Das wäre aber kein Problem, wenn die Werte nicht erst über EvalSpecials gehen müssten, sondern direkt als Referenzen an AnalyzePerlCommand übergeben werden könnten.

Ich habe daher mal überlegt, wie man die Features, die ich für eine zentrale Eval-Funktion von HTTPMOD und Modbus bräuchte, doch noch in AnalyzePerlCommand bekommen könnte.
Grundidee ist dabei dass man eine Hash-Referenz für die bereitzustellenden Variablen direkt an AnalyzePerlCommand  übergeben kann.
Mir ist klar, dass das nicht ganz so schöne Überschneidungen zu EvalSpecials mit sich bringt, aber ich wollte die Idee dennoch mal weitergeben. Vielleicht passt es ja doch.

Im Prinzip reichen ein paar zusätzliche Zeilen (mit ### new ### gekennzeichnet):


#####################################
sub
AnalyzePerlCommand($$;$)
{
  my ($cl, $cmd, $calledFromChain) = @_; # third parmeter is deprecated

  return "Forbidden command $cmd." if($cl && !Authorized($cl, "cmd", "perl"));

  $cmd =~ s/\\ *\n/ /g;               # Multi-line. Probably not needed anymore

  # Make life easier for oneliners:
  if($featurelevel <= 5.6) {
    %value = ();
    foreach my $d (keys %defs) {
      $value{$d} = $defs{$d}{STATE}
    }
  }
  my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) =
        localtime(gettimeofday());
  $month++; $year+=1900;
  my $today = sprintf('%04d-%02d-%02d', $year,$month,$mday);
  my $hms = sprintf("%02d:%02d:%02d", $hour, $min, $sec);
  my $we = IsWe(undef, $wday);

  if($evalSpecials) {
    $cmd = join("", map { my $n = substr($_,1); # ignore the %
                          my $v = $evalSpecials->{$_};
                          $v =~ s/(['\\])/\\$1/g;
                          "my \$$n='$v';";
                        } keys %{$evalSpecials})
           . $cmd;
    # Normally this is deleted in AnalyzeCommandChain, but ECMDDevice calls us
    # directly, and combining perl with something else isnt allowed anyway.
    $evalSpecials = undef if(!$calledFromChain);
  }

  ### new ###
  # new block to allow arrays and hashes to be passed to AnalyzePerlCommand as third parameter
  my $arg_ref = $calledFromChain;     # better rename above already
  if (ref ($arg_ref) eq 'HASH') {
    my $assign = '';
    foreach my $key (keys %{$arg_ref}) {
        my $type   = ref $arg_ref->{$key};
        my $vName  = substr($key,1);
        if ($type eq 'SCALAR') {
            $assign .= "my \$$vName =  \${\$arg_ref->{'$key'}};";
        } elsif ($type eq 'ARRAY') {
            $assign .= "my \@$vName =  \@{\$arg_ref->{'$key'}};";
        } elsif ($type eq 'HASH') {
            $assign .= "my \%$vName =  \%{\$arg_ref->{'$key'}};";
        }
    }
    $cmd = $assign . $cmd;
  }
  ### end new feature block ###

  $cmdFromAnalyze = $cmd;
  local $SIG{__WARN__} = sub { Log 1, "$arg_ref->{action} with expresion $cmd created warning: @_"; }
      if ((ref ($arg_ref) eq 'HASH') && ($arg_ref->{action}));
  my $ret = eval $cmd;
  if($@) {
    $ret = $@;
    Log 1, ($arg_ref->{action} ? "$arg_ref->{action} created " : '') . "ERROR evaluating $cmd: $ret";
  }
  $cmdFromAnalyze = undef;
  return $ret;
}


der Aufruf in HTTPMOD würde dann z.B. so aussehen:

my $erg = AnalyzePerlCommand(undef, $code, {'@val2' => \@val2, '$val' => \$val, '%testHash' => \%testHash});       


Falls das soweit nicht abwegig ist, würde ich gerne noch ein zweites Feature behalten:
Bisher Logge ich in HTTPMOD, Modbus und FReplacer bei Errors und Warnings aus Perl-Expressions auch einen Kontext als Hinweis, in welchem Attribut / Feature die fehlerhafte Expression zu finden ist. Das hat mir bisher im Support / bei der Fehlersuche sehr geholfen.

Um das in AnalyzePerlCommand auch unter zu bekommen reicht eigentlich eine Anweisung vor dem eval (auch auf obiger Erweiterung basierend) und eine entsprechende Ergänzung bei der Fehler-Protokollierung:

  local $SIG{__WARN__} = sub { Log 1, "$arg_ref->{action} with expresion $cmd created warning: @_"; }
      if ((ref ($arg_ref) eq 'HASH') && ($arg_ref->{action}));

Den Kontext würde man dann in dem neuen Hash einfach als weiteren Eintrag hinzufügen:

action => 'reading53Expr'


ich habe auch kein Problem damit, eine solche Funktion nur für HTTPMOD, Modbus und FReplacer in ein Modul zu packen, aber schöner wäre es natürlich wenn AnalyzePerlCommand auch Arrays oder Hashes für eine Expression bereitstellen könnte.

Was meinst Du?

Gruss
   Stefan

rudolfkoenig

Danke fuer die "Grundidee", das habe ich uebernommen, der Eingriff war deutlich kleiner, als ich das befuerchtet habe. :)
Es geht aber weiterhin ueber EvalSpecials, ich wollte keine Parallelwege oeffnen.

Wg. Kontext sehe ich keinen Handlungsbedarf: AnalyzePerlCommand setzt $cmdFromAnalyze, und das wird bei WARNING ausgegeben.
Mit "attr global stacktrace 1" sogar mit stacktrace.

Getestet habe ich mit
use strict;
use warnings;

sub
test_Initialize($)
{
  my ($hash) = @_;
  $hash->{DefFn} = "test_Define";
}

sub
test_Define($$)
{
  my ($hash, $def) = @_;
  my @args = split(" ", $def, 3);
  my $code = join(" ", @args[2..$#args]);

  $code = EvalSpecials($code, ("%S"=>$args[0], "%A"=>\@args, "%H"=>$hash));
  my $ret = AnalyzePerlCommand(undef, $code);
}

1;

und
define test test { Log 1, "S:$S A:".join(",",@A).' H:'.join(",",keys %H) }

StefanStrobel

Vielen Dank!
so ist es nochmal eleganter.
Das mit dem Kontext hätte dem im Log angezeigten Code noch einen Namen gegeben (bei HTTPMOD der Name des Attributes, aus dem der Code stammt - so wie Du es bei CheckRegexp auch vorgesehen hast) aber so muss der Anwender eben seinen Perl-Code wiedererkennen. Das sollte aber auch machbar sein ;-)

Gruss
   Stefan