Run multiple SHELL commands within same SHELL - perl

I am working on a legacy and confined system, where I do not have any known way to get anything installed. Now, I need to run multiple SHELL commands on a same shell. For example, the 2nd command should be aware of any changes done to the SHELL environment by the previous command. (like Environment variable manipulation). The system does not have perl expect module available, so I am trying to use IPC::Open3 to open shell and run commands and checking the stdout and stderr for expected strings. This works fine, for only the first function call of runner function and not the 2nd call.
Here is my code, dumb down version:
#!/usr/bin/perl
use IPC::Open3;
use utf8;
#First expect script
my $pid = open3(*IN, *OUT, *ERR, "/bin/bash");
my #output_arr;
my #error_arr;
sub runner {
my ($PID,$CMD,$REGEX) = #_;
print IN "$CMD\n";
close IN;
waitpid $PID, 0;
my $cmd_output = do { local $/; <OUT> };
my $cmd_error = do { local $/; <ERR> };
if ($cmd_output =~ m/$REGEX/ or $cmd_error =~ m/$REGEX/) {
print "$CMD output contains $REGEX\n"
}
else
{
print "$CMD output does not contain $REGEX, command output is $cmd_output ,error is $cmd_error\n"
}
push #output_arr, $cmd_output;
push #error_arr, $cmd_error;
return ($cmd_output, $cmd_error);
}
# This data will come from other progams.
#this works
my ($date_output, $date_error) = runner($pid, "LC_ALL=C date", qr/2023/s);
#this never works
my ($date_output, $date_error) = runner($pid, "LC_ALL=C date", qr/Jan/s);
The output:
LC_ALL=C date output contains (?^s:2023)
LC_ALL=C date output does not contain (?^s:Jan), command output is ,error is

Related

Error: perl Script is running twice

I have written a small script from which I'm calling another script.
Code:
package.PL
use strict;
no warnings 'experimental::smartmatch';
use feature qw(switch);
print"\nPlease enter Perl Installation Path\n";
my $path=<>;
$path=~ s/^\s+|\s+$//;
while(1){
print "\nEnter your Choice : \n";
print "1.Premigration Script for active records\n";
print "2.Premigration Script for archival records\n";
print "3.Post Migration Script\n";
print "4.Cleanup Script\n";
print "5.Exit\n";
my $input=<>;
given($input){
when(1) {system("$path/perl export_from_ddts.pl configfile_active.ini");system("$path/perl convert_to_csv.pl configfile_active.ini");}
when(2) {system("$path/perl export_from_ddts.pl configfile_archived.ini");system("$path/perl convert_to_csv.pl configfile_archived.ini");}
when(3) {system("$path/perl post_migration.pl configfile_active.ini");}
when(4) { system("$path/perl cleanup.pl");}
default {
if($input > 4){
print "\nYou want to exit the menu? y/n\n";
my $state=<>;
$state =~ s/^\s+|\s+$//g ;
if($state eq 'y'){
last;
}
else{
continue;
}
}
}
}
}
If I call any script from Package.pl, It is running twice.
For example: If I select option 1 to run pre-migration script for active projects, I'm getting same output twice.
perl version:5.18.1
I'm running on another perl installation(not system perl) in linux.
At first you could use $^X for the PATH of Perl or $EXECUTABLE_NAME when you use use English
You see the output twice? - Your code is ok. Why do you use no warnings 'experimental::smartmatch when you don use any smartmatch operators?
I have tried to reconstruct your code but its working for me.

Looping through data provided by Net::SSH2

I am using the following lib Net::SSH2
I can connect to my device and get the output OK in most case. Below is the relevant code:
sub logIntoDevice
{
my $self = shift;
my ($ssh2) = #_;
if(! $ssh2->connect($self->deviceIP))
{
say "Failed to connect to device:",$self->deviceIP;
$ssh2->disconnect();
exit 0;
}
if(! $ssh2->auth_password($self->usr, $self->pass))
{
say "Authentication Fail to ",$self->deviceIP;
$ssh2->disconnect();
exit 0;
}
my $channel = $ssh2->channel();
$channel->blocking(0);
$channel->shell();
return $channel;
}
sub sendCmd
{
my $self = shift;
my ($cmd,$channel) = #_;
my #cmdOutput;
print $channel "$cmd\n";
while (<$channel>)
{
chomp $_;
push(#cmdOutput, $_);
}
return #cmdOutput;
}
So below are the cmd i sent to the sub's. They work fine and the output is write to file OK.
$self->writeToFile($fileName,$self->sendCmd("show clock",$channel));
$self->writeToFile($fileName,$self->sendCmd("\n",$channel));
$self->writeToFile($fileName,$self->sendCmd("dir",$channel));
with the exception of when i sent the following cmd:
$self->writeToFile($fileName,$self->sendCmd("sh run",$channel));
the output of the cmd on the device using putty is:
sh run
Building configuration...
Current configuration : 16575 bytes
!
!
!Last configuration change at 16:37:19 CET+1 Sat Mar 15 2014
.....
but in the log file all you see is
sh run
Building configuration...
so the issue is the blank lines after the Building configuration output make the while (<$channel>) think its the end of the output.
My issue is I cant figure a way to loop through the data without using a While loop.
UPDATE
Ok come up with this solution but seems very clunky. must be a better way if doing this
sub sendCmd
{
my $self = shift;
my ($cmd,$channel) = #_;
my #cmdOutput;
my $currPrompt;
#get prompt. i am sure there is a better way!!! just cant figure it out
print $channel "\n";
while (<$channel>)
{
$currPrompt = $_;
}
print $channel "$cmd\n";
while(42)
{
my $inerOutput;
while (<$channel>)
{
chomp $_;
$inerOutput = $_;
push(#cmdOutput, $_);
}
if($inerOutput ne $currPrompt)
{
sleep(7);
}
else
{
last;
}
}
return #cmdOutput;
}
I don't think your issue is blank lines. Most likely, the issue is that you are using non-blocking mode, and it takes time for the device to perform the command. So you are getting an empty line (or an undef) after "Building configuration..." is read because there is no extra output produced yet.
I would use Net::SSH2's poll method with a timeout, which will let you know when there is something to read. If "sh run" takes substantially longer than other commands you are issuing, your sendCmd method needs to be aware of this, and allow more time to pass before it decides no more output is coming its way.
Alternatively, you can (as is the custom when using, for example, Net::Telnet) wait for more output until you see the prompt, whatever the prompt is for the device in question,
and then you will know that the command has finished its execution.
Net::SSH2->poll is deprecated as result of libss2_poll deprecation

Perl Expect with FreezeThaw

I am converting an interactive command line tool to a web application, with the tool as the backend.
I take in user commands (using AJAX) and call a perl CGI script that extracts the command.
I then use expect to send the command to the process, collect the output and pass that to the resultant html page.
The first command that the user inputs executes fine.
The next commands aren't executed.
I am using FreezeThaw to freeze the expect object after the first request and then to thaw it for the following requests. It freezes fine, but doesn't thaw.
Here's my code:
use strict;
use warnings;
use CGI;
use Expect;
use FreezeThaw qw(freeze thaw);
if ( -e "logFile" ) {
##Log file exists, just run the command after retrieving the object
##Retrieve object here
my ($expectObject) = thaw( $params{'object'} );
if ( $command eq 'exit' ) {
#exit
}
}
else {
print "log NOT exists!!";
##Log file doesn't exist, spawn a new process and loop
my $expectObject = Expect->spawn("command") || die "\nCannot spawn: $!\n";
$expectObject->expect( 15, "prompt>" );
$expectObject->send("$command\r");
$expectObject->expect( 15, "stile>" );
$output = $expectObject->before();
print "<br>$output<br>";
##Persist object here in file
my $serialized = freeze($expectObject);
##Write serialized object to file
die "Serialization Error (write):\n$!" if ( !addParameter( "$workingDir/$folderName", "object", $serialized ) );
}
Any ideas why is it failing..?
If a Perl CGI program ends, it will destroy all spawned process if they does not daemonize itself.
Use mod_perl or other persistent mechanism to keep open a 'shell/command' or execute all commands one by one.

How can I get user input without waiting for enter in Perl?

I am trying to make an interactive shell script in Perl.
The only user input I can find is the following:
$name = <STDIN>;
print STDOUT "Hello $name\n";
But in this the user must always press enter for the changes to take effect.
How can I get the program to proceed immediately after a button has been pressed?
From perlfaq8's answer to How do I read just one key without waiting for a return key?
:
Controlling input buffering is a remarkably system-dependent matter. On many systems, you can just use the stty command as shown in getc in perlfunc, but as you see, that's already getting you into portability snags.
open(TTY, "+</dev/tty") or die "no tty: $!";
system "stty cbreak </dev/tty >/dev/tty 2>&1";
$key = getc(TTY); # perhaps this works
# OR ELSE
sysread(TTY, $key, 1); # probably this does
system "stty -cbreak </dev/tty >/dev/tty 2>&1";
The Term::ReadKey module from CPAN offers an easy-to-use interface that should be more efficient than shelling out to stty for each key. It even includes limited support for Windows.
use Term::ReadKey;
ReadMode('cbreak');
$key = ReadKey(0);
ReadMode('normal');
However, using the code requires that you have a working C compiler and can use it to build and install a CPAN module. Here's a solution using the standard POSIX module, which is already on your system (assuming your system supports POSIX).
use HotKey;
$key = readkey();
And here's the HotKey module, which hides the somewhat mystifying calls to manipulate the POSIX termios structures.
# HotKey.pm
package HotKey;
#ISA = qw(Exporter);
#EXPORT = qw(cbreak cooked readkey);
use strict;
use POSIX qw(:termios_h);
my ($term, $oterm, $echo, $noecho, $fd_stdin);
$fd_stdin = fileno(STDIN);
$term = POSIX::Termios->new();
$term->getattr($fd_stdin);
$oterm = $term->getlflag();
$echo = ECHO | ECHOK | ICANON;
$noecho = $oterm & ~$echo;
sub cbreak {
$term->setlflag($noecho); # ok, so i don't want echo either
$term->setcc(VTIME, 1);
$term->setattr($fd_stdin, TCSANOW);
}
sub cooked {
$term->setlflag($oterm);
$term->setcc(VTIME, 0);
$term->setattr($fd_stdin, TCSANOW);
}
sub readkey {
my $key = '';
cbreak();
sysread(STDIN, $key, 1);
cooked();
return $key;
}
END { cooked() }
1;
You can use the Term::ReadKey module to check for a keypress.

How can I properly import the environment from running a subcommand in Perl?

In importing the environment from a subcommand, I want to add all environment variables exported from a bash script to a hash. When program gets run, it will set up some variables and export them. I'd like to save those variables in the Perl script for later. However I don't want to take the bash functions defined in the subcommand. Currently, I have a block like:
foreach (`program; env`)
{
next if /^\(\)/;
my ($a, $b) = split("=", $_);
if( /^(\w+)=(.*)$/ ) {
$hash{$1} = $2;
}
}
Is there a better way to do this? I'm not sure if matching the initial () is safe. Bonus points for handling newlines in environment variables, which I'm just closing my eyes for right now.
What you want is there: Shell-EnvImporter
An example:
use Shell::EnvImporter;
# Import environment variables exported from a shell script
my $sourcer = Shell::EnvImporter->new(
file => $filename,
);
my $result = $sourcer->run() or die "Run failed: $#";
I am assuming that the environment variables after program has executed are not same as the environment passed to it (which you can find in %ENV as explained in jeje's answer.
I am by no means knowledgeable about bash, so I am only going to address the part of the question about parsing the output of env.
#!/usr/bin/perl
use strict;
use warnings;
use autodie qw( open close );
$ENV{WACKO} = "test\nstring\nwith\nnewlines\n\n";
my %SUBENV;
open my $env_h, '-|', 'env';
my $var;
while ( my $line = <$env_h> ) {
chomp $line;
if ( my ($this_var, $this_val) = $line =~ /^([^=]+)=(.+)$/ ) {
if ( $this_val =~ /^\Q()\E/ ) {
$var = q{};
next;
}
$var = $this_var;
$SUBENV{ $var } = $this_val;
}
elsif ( $var ) {
$SUBENV{ $var } .= "\n$line";
}
}
use Data::Dumper;
print Dumper \%SUBENV;
This should be fine for getting all of the environment variables.
for(`program; env`){
if( /^([^=]+)=(.*)$/ ) {
$hash{$1} = $2;
}
}
If you want to start with a clean slate this might work better.
for(`env -i bash -c "program; env"`){
next if /\(\)/;
if( /^([^=]+)=(.*)$/ ) {
$hash{$1} = $2;
}
}
env -i makes it's subcommand start off with a clean slate.
It calls bash with the -c argument, and the commands to run. We need to do that because otherwise the second env wouldn't get the environment variables from the program.