#!/usr/bin/env perl use lib 'lib'; use History::SQLite; use Environment::SQLite; use Term::ReadLine; use Cwd; use File::HomeDir; use CustomCommands; use POSIX qw(getlogin getuid setsid tcsetpgrp); # there is only one setsid(); # Start a new session tcsetpgrp(0, getpgrp()); # Set TTY foreground process group # some env vars $ENV{HOME} ||= File::HomeDir->my_home; $ENV{USER} ||= getlogin() || (getpwuid(getuid()))[0] || 'user'; $ENV{LOGNAME} ||= $ENV{USER}; $ENV{SHELL} ||= $0; # Path to pshell.pl or compiled binary $ENV{PATH} ||= '/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin'; $ENV{TERM} ||= 'xterm-256color'; # we a login shell? my $is_login_shell = ($0 =~ /^-/ || $ARGV[0] eq '-l'); if ($is_login_shell) { # Clear inherited environment for security, except essentials %ENV = ( HOME => $ENV{HOME}, USER => $ENV{USER}, LOGNAME => $ENV{LOGNAME}, SHELL => $ENV{SHELL}, TERM => $ENV{TERM}, ); # Reload system and user configs } # system wide env vars # Parse /etc/environment silently if (-r '/etc/environment') { open my $fh, '<', '/etc/environment' or do { warn "Could not open /etc/environment: $!" if $is_login_shell; next; }; while (my $line = <$fh>) { chomp $line; next unless $line =~ /^(\w+)=(.*)$/; $ENV{$1} = $2 unless exists $ENV{$1}; } close $fh; } # Source /etc/profile silently if (-r '/etc/profile') { # Run /etc/profile in a subshell, capture env output, and suppress other prints my $env_output = qx(/bin/sh -c '. /etc/profile >/dev/null 2>&1 && env'); if ($? == 0) { while ($env_output =~ /^(\w+)=(.*)$/mg) { $ENV{$1} = $2 unless exists $ENV{$1}; } } else { warn "Could not source /etc/profile: $!" if $is_login_shell; } } # Initialize command history and environment my $history = History::SQLite->new( db_path => 'pshell_history.db' ); my $env = Environment::SQLite->new( db_path => 'pshell_env.db' ); # Signal handling setup $SIG{INT} = sub { print "\n"; }; $SIG{CHLD} = 'IGNORE'; print "Perl Shell (pshell) - Type 'exit' to quit\n"; my $term = Term::ReadLine->new('pshell'); $term->ornaments(0); # Workaround for Ctrl-R binding with Term::ReadLine::Gnu $term->add_defun('history-search', sub { my ($count, $key) = @_; my $search_term = $term->readline('Search history: '); return unless defined $search_term && length $search_term; my @matches = grep { $_ =~ /\Q$search_term\E/i } $history->get_all; if (@matches) { print "\nMatching commands:\n"; for my $i (0..$#matches) { print sprintf("%2d: %s\n", $i+1, $matches[$i]); } my $choice = $term->readline('\nSelect command to run (number or Enter to cancel): '); if ($choice =~ /^\d+$/ && $choice > 0 && $choice <= @matches) { $term->addhistory($matches[$choice-1]); $term->replace_line($matches[$choice-1]); $term->redisplay; } } else { print "\nNo matching commands found\n"; } return 0; }); $term->parse_and_bind('"\C-r": history-search'); # Load previous history my @history = $history->get_all; $term->addhistory($_) for @history; # Load environment variables my %env_vars = $env->get_all; $ENV{$_} = $env_vars{$_} for keys %env_vars; # Load and execute .pshellrc if it exists my $rc_file = File::HomeDir->my_home . '/.pshellrc'; if (-e $rc_file && -r $rc_file) { open my $fh, '<', $rc_file or warn "Could not open $rc_file: $!"; while (my $line = <$fh>) { chomp $line; next unless length $line; if ($line =~ /^(\w+)=(.*)$/) { $ENV{$1} = $2; $env->set($1, $2); } else { system($line); } } close $fh; } while (1) { my $cwd = getcwd(); my $prompt = "pshell:$cwd> "; my $command = $term->readline($prompt); last unless defined $command; chomp $command; last if $command =~ /^exit$/i; # Skip empty commands next unless length $command; # Handle command my $cmd_status = CustomCommands::handle($command, $env); if ($cmd_status >= 0) { # 1=handled, 2=system command # Save valid commands to history $history->add($command); $term->addhistory($command); next; } # Command not found print "Command not found: $command\n"; next; }