Index: ossp-pkg/ac/README RCS File: /v/ossp/cvs/ossp-pkg/ac/README,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/ac/README,v' | diff -u /dev/null - -L'ossp-pkg/ac/README' 2>/dev/null --- ossp-pkg/ac/README +++ - 2024-05-10 19:23:53.893353291 +0200 @@ -0,0 +1,3 @@ + + AutoConnect + Index: ossp-pkg/ac/TODO RCS File: /v/ossp/cvs/ossp-pkg/ac/TODO,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/ac/TODO,v' | diff -u /dev/null - -L'ossp-pkg/ac/TODO' 2>/dev/null --- ossp-pkg/ac/TODO +++ - 2024-05-10 19:23:53.895964509 +0200 @@ -0,0 +1,39 @@ + +- "lock" command for locking the session + +- "not" (!) implementing in grants + +- error on /DNS +ac: connection establishing +Use of uninitialized value in hash element at /u/rse/ac/src/ac line 1216. +Use of uninitialized value in concatenation (.) or string at /u/rse/ac/src/ac line 1216. +ac:ERROR: invalid login method "" +ac:ERROR: Can't call method "delwin" on an undefined value + +- support not in grants with "!xx" +- access levels/tags: + grant { @gsoc[0] } { @is }; + grant { @gsoc[1] } { rse cschug }; +- display order + order { @is @gsoc } { @is }; + order { @gsoc @is } { @gsoc }; +- switch user + superuser { rse cschug }; + grant { SUPERUSER } { rse }; +- who's online, last login, busy time, ...$ who +- options: -+include, -+shell$(cmd), -+overrideentry +- remove special case of "hostname" and make this the first argument of login args +- shell-style $(cmd) einbauen (und dafuer $ENVs rauswerfen?) +- usage of #1 for backreferences in expected strings should be locally scoped?! +- more expect methods + +- SIGWINCH support +- provide last-login information +- option -g for starting in CUI and staying there + +- add ac.pod documentation + +- better code commenting +- better consistrency checks +- better error checking, especially line/col info on parse errors + Index: ossp-pkg/ac/ac RCS File: /v/ossp/cvs/ossp-pkg/ac/ac,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/ac/ac,v' | diff -u /dev/null - -L'ossp-pkg/ac/ac' 2>/dev/null --- ossp-pkg/ac/ac +++ - 2024-05-10 19:23:53.898532195 +0200 @@ -0,0 +1,1457 @@ +#!/usr/opkg/bin/perl -w +## +## OSSP ac -- Auto Connection +## Copyright (c) 2003 The OSSP Project +## Copyright (c) 2003 Cable & Wireless Germany +## Copyright (c) 2003 Ralf S. Engelschall +## +## This file is part of OSSP ac, a tool for automated login +## sessions which can be found at http://www.ossp.org/pkg/tool/ac/. +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version +## 2.0 of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this file; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +## USA, or contact The OSSP Project +## +## ac: program implementation (syntax: Perl 5.x) +## + +require 5.008; +use strict; +$|++; + +# load a plethora of Perl modules +# (this really requires OpenPKG because of +# patches in perl-curses for Curses::UI!) +use IO; # from OpenPKG "perl" +use POSIX; # from OpenPKG "perl" +use Getopt::Long; # from OpenPKG "perl" +use Data::Dumper; # from OpenPKG "perl" +use Parse::RecDescent; # from OpenPKG "perl-parse" +#use Term::ReadLine; # from OpenPKG "perl-term" (loaded deferred) +#use Term::ReadLine::Gnu; # from OpenPKG "perl-term" (loaded deferred) +#use Curses; # from OpenPKG "perl-curses" (loaded deferred) +#use Curses::UI; # from OpenPKG "perl-curses" (loaded deferred) +#use Expect; # from OpenPKG "perl-sys" (loaded deferred) + +# configure optional debugging +$Data::Dumper::Purity = 1; +$Data::Dumper::Indent = 1; +$Data::Dumper::Terse = 1; + +# fixed program information +my $my = { + prog_name => "ac", + prog_vers => "2.0.0", + prog_date => "11-Apr-2003", + prog_global => "/u/rse/ac/src/config", + prog_homedir => "$ENV{HOME}/.ac", + prog_config => "$ENV{HOME}/.ac/config", + prog_complete => "$ENV{HOME}/.ac/.complete", + prog_history => "$ENV{HOME}/.ac/.history", + prog_grammar => "$ENV{HOME}/.ac/.grammar", + prog_tmpfile => "$ENV{HOME}/.ac/.tmp.$$", + user_name => ($ENV{USER} || $ENV{LOGNAME} || "unknown"), +}; + +## _________________________________________________________________________ +## +## Command Line Option Parsing +## _________________________________________________________________________ +## + +# default command line options +my $opt = { + version => 0, + verbose => 0, + debug => 0, + update => 0, + config => "", + help => 0, + define => [], +}; + +# global exception handling support +$SIG{__DIE__} = sub { + my ($err) = @_; + $err =~ s|\s+at\s+.*||s if (not $opt->{verbose}); + print STDERR "$my->{prog_name}:ERROR: $err ". ($! ? "($!)" : "") . "\n"; + exit(1); +}; + +# parse command line options +Getopt::Long::Configure("bundling"); +my %getopt_spec = ( + 'V|version' => \$opt->{version}, + 'v|verbose' => \$opt->{verbose}, + 'd|debug' => \$opt->{debug}, + 'h|help' => \$opt->{help}, + 'u|update' => \$opt->{update}, + 'D|define=s' => $opt->{define}, + 'c|config=s' => \$opt->{config}, +); +my $result = GetOptions(%getopt_spec) + || die "command line option parsing failed"; +if ($opt->{help}) { + print "usage: $my->{prog_name} [] \n" . + "available options are:\n" . + " -v,--verbose enable verbose run-time mode\n" . + " -u,--update update Bash command completion cache\n" . + " -V,--version print program version\n" . + " -h,--help print out this usage page\n" . + " -D,--define NAME=VAL set (and overwrite) variable definitions\n" . + " -c,--config FILE read this configuration file only\n"; + exit(0); +} +if ($opt->{version}) { + print "$my->{prog_name} $my->{prog_vers} ($my->{prog_date})\n"; + exit(0); +} + +## _________________________________________________________________________ +## +## Configuration Parsing +## _________________________________________________________________________ +## + +# $cf = { +# user => { +# -order => [ rse, ... ], +# rse => { realname => "...", description => ... }, +# ... +# }, +# group => { +# -order => [ en, ... ], +# en => { description => "...", members => [...] }, +# ... +# }, +# host => { +# -order => [ en1, ... ], +# en1 => { hostname => "...", ... }, +# ... +# }, +# domain => { +# -order => [ en, ... ], +# en => { description => "...", members => [...] }, +# ... +# }, +# grant => [ +# [ [ "...", ... ], [ "...", ... ] ], +# ... +# ], +# method => { +# ssh => [ [], [], ... ], +# ... +# } +# }; + +# prepare in-core configuration representation +use vars qw($cf); +$cf = { + user => { -order => [] }, + group => { -order => [] }, + host => { -order => [] }, + domain => { -order => [] }, + grant => [], + method => {} +}; + +# parsing utility function: read configuration file +sub config_load { + my ($ctx, $filename) = @_; + + if ($filename !~ m|^/|s) { + # make absolute out of relative path + $filename = $ctx->{cwd}->[-1]."/".$filename; + $filename = &path_canonicalize($filename); + } + my $io = new IO::File "<$filename" + or die "cannot open configuration file \"$filename\""; + my $config = ''; + $config .= $_ while (<$io>); + $io->close; + $config =~ s|^\s*#.*?$||mg; + return $config; +} + +# parsing utility function: expand a token +sub expand_token { + my ($ctx, $token) = @_; + $token =~ s/\$\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{def}->{$1} || $ENV{$1} || ''/sge; + $token =~ s/\%\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{def}->{$1} || $ctx->{env}->{$1} || ''/sge; + $token =~ s/\@\{([^\}]+)\}/&base64_decode($1)/sge; + $token =~ s/\\t/\t/sg; + $token =~ s/\\r/\r/sg; + $token =~ s/\\n/\n/sg; + $token =~ s/\\c(.)/eval "\\c$1"/sge; + return $token; +} + +# the top-down parsing grammar itself +my $grammar = q[ + # create local variable for holding parsing context + { use vars qw($ctx); $ctx = ""; } + + # start rule (for explicit context init and EOF handling) + start: { $ctx = $arg[0]; } + + config /^\Z/ + | + + # whole configuration + config: directive(s) + | + + # single configuration directive + directive: include + | scope + | set + | user + | group + | host + | domain + | grant + | method + | "" + | + + # directive: "set ;" + set: "set" token token ";" { + $ctx->{env}->{$item[2]} = &::expand_token($ctx, $item[3]); + } + | + + # directive: "include ;" + include: "include" token ";" { + my $file = $item[2]; + my $dir = $file; + $dir =~ s|/[^/]+$||s; + $dir = $ctx->{cwd}->[-1]."/".$dir if ($dir !~ m|^/|s); + $dir = &::path_canonicalize($dir); + #$text = "scope \"$dir\" { " . &::config_load($ctx, $file) . "}; " . $text; + $text = &::config_load($ctx, $file) . $text; + Parse::RecDescent::LineCounter::resync($thisline); + } + | + + # directive: "scope { ... };" + scope: "scope" token "{" { + push(@{$ctx->{cwd}}, $item[2]); + } + config { + pop(@{$ctx->{cwd}}); + } "}" ";" + | + + # directive: "user ;" + user: "user" token token token ";" { + die "line $thisline (column $thiscolumn): " . + "user \"$item[2]\" already defined" + if (defined($ctx->{cf}->{user}->{$item[2]})); + push(@{$ctx->{cf}->{user}->{-order}}, $item[2]); + $ctx->{cf}->{user}->{$item[2]} = { + 'realname' => $item[3], + 'email' => $item[4] + }; + } + | + + # directive: "group { [ ...] };" + group: "group" token token "{" token(s?) "}" ";" { + die "line $thisline (column $thiscolumn): " . + "group \"$item[2]\" already defined" + if (defined($ctx->{cf}->{group}->{$item[2]})); + push(@{$ctx->{cf}->{group}->{-order}}, $item[2]); + $ctx->{cf}->{group}->{$item[2]} = { + 'description' => $item[3], + 'members' => $item[5] + }; + } + | + + # directive: "host { ... };" + host: + | "host" token "{" host_param[$param](s) "}" ";" { + die "line $thisline (column $thiscolumn): " . + "host \"$item[2]\" already defined" + if (defined($ctx->{cf}->{host}->{$item[2]})); + push(@{$ctx->{cf}->{host}->{-order}}, $item[2]); + $ctx->{cf}->{host}->{$item[2]} = $param; + } + | + host_param: /(hostname|purpose|system|platform|location)/ token ";" { + $arg[0]->{$item[1]} = $item[2]; + } + | /(login|domain)/ token(s) ";" { + $arg[0]->{$item[1]} = $item[2]; + } + | + + # directive: "domain { [ ...] };" + domain: "domain" token token "{" token(s?) "}" ";" { + die "line $thisline (column $thiscolumn): " . + "domain \"$item[2]\" already defined" + if (defined($ctx->{cf}->{domain}->{$item[2]})); + push(@{$ctx->{cf}->{domain}->{-order}}, $item[2]); + $ctx->{cf}->{domain}->{$item[2]} = { + 'description' => $item[3], + 'members' => $item[5] + }; + } + | + + # directive: "grant { [ ...] } { [ ...] };" + grant: "grant" "{" token(s) "}" "{" token(s) "}" ";" { + push(@{$ctx->{cf}->{grant}}, [ $item[3], $item[6] ]); + } + | + + # directive: "method { [...] };" + method: "method" token "{" script "}" ";" { + die "line $thisline (column $thiscolumn): " . + "method \"$item[2]\" already defined" + if (defined($ctx->{cf}->{method}->{$item[2]})); + $ctx->{cf}->{method}->{$item[2]} = $item[4]; + } + | + + # login script + script: command ";" script { unshift(@{$item[3]}, $item[1]); + $return = $item[3]; } + | command { $return = $item[1]; } + | + + # login script: scripting directives + command: "system" token { $return = [ $item[1], $item[2] ]; } + | "spawn" token { $return = [ $item[1], $item[2] ]; } + | "timeout" token { $return = [ $item[1], $item[2] ]; } + | "send" token { $return = [ $item[1], $item[2] ]; } + | "expect" expect_args { $return = [ $item[1], $item[2] ]; } + | "sleep" token { $return = [ $item[1], $item[2] ]; } + | "repeat" { $return = [ $item[1] ]; } + | "interact" { $return = [ $item[1] ]; } + | if_clause { $return = $item[1]; } + | "" { $return = [ ]; } + | + + # login script: expect argument(s): "regex [ script [ regex script ... ]]" + expect_args: expect_arg_std(1..) { $return = $item[1]; } + | expect_arg_abr { $return = [ $item[1] ]; } + | + expect_arg_std: regex "{" script "}" { $return = [ $item[1], $item[3] ]; } + | + expect_arg_abr: regex { $return = [ $item[1], [] ]; } + | + + # if clause + if_clause: "if" "(" expr ")" "{" script "}" ("else" "{" script "}")(?) { + $return = [ $item[1], $item[3], $item[6], $item[10] ]; + } + | + + # boolean expression + # (clean and obvious, but for top-down parsing + # not usable version because of left-recursion) + #expr: "(" expr ")" { $return = $item[2]; } + # | /[!-]/ expr { $return = [ "op:$item[1]", $item[2] ]; } + # | expr /[.*\/%+-]/ expr { $return = [ "op:$item[2]", $item[1], $item[3] ]; } + # | expr /(==|!=|<|<=|>=|>)/ expr { $return = [ "op:$item[2]", $item[1], $item[3] ]; } + # | expr "=~" regex { $return = [ "op:$item[2]", $item[1], $item[3] ]; } + # | token { $return = [ "tok", $item[1] ]; } + # | + + # boolean expression + # (ugly and confusing, but usable version, + # resulted through left-recursion elemination) + expr: "(" expr ")" { $return = $item[2]; } + term[$item[4]] { $return = $item[5]; } + | /[!-]/ expr { $return = [ "op:$item[1]", $item[2] ]; } + term[$item[3]] { $return = $item[4]; } + | token { $return = [ "tok", $item[1] ]; } + term[$item[2]] { $return = $item[3]; } + | + term: /[.*\/%+-]/ expr { $return = [ "op:$item[1]", $arg[0], $item[2] ]; } + term[$item[3]] { $return = $item[4]; } + | /(==|!=|<|<=|>=|>)/ expr { $return = [ "op:$item[1]", $arg[0], $item[2] ]; } + term[$item[3]] { $return = $item[4]; } + | "=~" regex { $return = [ "op:$item[1]", $arg[0], $item[2] ]; } + term[$item[3]] { $return = $item[4]; } + | {1} { $return = $arg[0]; } + | + + # regular expression token + regex: m{/((\\\\/|[^/])*)/} { $return = &::expand_token($ctx, $1); } + | token { $return = quotemeta($item[1]); } + | + + # plain text token + token: m/\"((\\\"|[^\"])*)\"/ { $return = &::expand_token($ctx, $1); } + | m/'((\\\\'|[^'])*)'/ { $return = &::expand_token($ctx, $1); } + | m/[^ \t\r\n;\{\}\(\)]+/ { $return = &::expand_token($ctx, $item[1]); } + | +]; + +# create configuration parser +if ($opt->{debug}) { + #$::RD_TRACE = 1; + $::RD_HINT = 1; + $::RD_ERRORS = 1; + $::RD_WARN = 1; +} +my $parser; +if (not $opt->{debug}) { + # speed up run-time processing by pre-compiling grammar + if (! -f "$my->{prog_grammar}") { + # perform pre-compilation operation + my $cwd = POSIX::getcwd(); + chdir($my->{prog_homedir}); + Parse::RecDescent->Precompile($grammar, "ac::grammar"); + my $io = new IO::File "; } + $io->close; + unlink("grammar.pm"); + chdir($cwd); + + # fixup pre-compilation result (at least necessary for P::RD <= 1.93) + $pm =~ s|(sub\s+new\s+\{\s+)my\s+(bless\()|$1$2|s; + $pm =~ s|$|\n1;\n|s; + + # store pre-compilation result for reuse + $io = new IO::File ">$my->{prog_grammar}" + or die "unable to write pre-compiled grammar to \"$my->{prog_grammar}\""; + $io->print($pm); + $io->close; + } + + # read pre-compiled grammar + require "$my->{prog_grammar}"; + import ac::grammar; + $parser = new ac::grammar; +} +else { + # slow approach: parse from scratch + $parser = new Parse::RecDescent ($grammar) + or die "unable to parse configuration parsing grammar"; +} + +# prepare parsing context +my $ctx = {}; +$ctx->{cf} = $cf; +$ctx->{cwd} = [ POSIX::getcwd() ]; +$ctx->{env} = {}; +$ctx->{def} = {}; +foreach my $define (@{$opt->{define}}) { + if ($define =~ m|^([a-zA-Z][a-zA-Z0-9_]*)(?:[=:](.*))?$|) { + my ($name, $value) = ($1, $2); + $value ||= ''; + $ctx->{def}->{$name} = $value; + } +} + +# on-the-fly generate top-level configuration +my $config = ''; +if ($opt->{config} ne '') { + # explicitly supplied configuration file + $config .= "include \"$opt->{config}\";"; +} +else { + # implicitly supplied global and local configuration files + if (-f $my->{prog_global}) { + $config .= "include \"$my->{prog_global}\";"; + } + if (-f $my->{prog_config}) { + $config .= "include \"$my->{prog_config}\";"; + } +} + +# recursively parse configuration +defined $parser->start($config, 1, $ctx) + or die "failed to parse configuration directives"; + +## _________________________________________________________________________ +## +## Configuration Post-Processing +## _________________________________________________________________________ +## + +# expand group references in community group memberships configuration +sub expand_group { + my ($name) = @_; + my $members = {}; + foreach my $member (@{$cf->{group}->{$name}->{members}}) { + if ($member =~ m|^\@(.+)$|s) { + die "reference to unknown group \"$1\" in group \"$name\"" + if (not defined($cf->{group}->{$1})); + my $M = &expand_group($1); + map { $members->{$_} = 1 } (@{$M}); + } + else { + die "reference to unknown user \"$member\" in group \"$name\"" + if (not defined($cf->{user}->{$member})); + $members->{$member} = 1; + } + } + return [ keys(%{$members}) ]; +} +foreach my $name (keys(%{$cf->{group}})) { + next if ($name eq '-order'); + $cf->{group}->{$name}->{members} = &expand_group($name); +} + +# extend domain memberships with reverse/per-host domain specification +foreach my $host (keys(%{$cf->{host}})) { + next if ($host eq '-order'); + if (defined($cf->{host}->{$host}->{domain})) { + foreach my $domain (@{$cf->{host}->{$host}->{domain}}) { + die "unknown domain \"$domain\"" + if (not defined($cf->{domain}->{$domain})); + push(@{$cf->{domain}->{$domain}->{members}}, $host); + } + } +} + +# expand domain references in system domain memberships configuration +sub expand_domain { + my ($name) = @_; + my $members = {}; + foreach my $member (@{$cf->{domain}->{$name}->{members}}) { + if ($member =~ m|^\@(.+)$|s) { + die "reference to unknown domain \"$1\" in domain \"$name\"" + if (not defined($cf->{domain}->{$1})); + my $M = &expand_domain($1); + map { $members->{$_} = 1 } (@{$M}); + } + else { + die "reference to unknown host \"$member\" in domain \"$name\"" + if (not defined($cf->{host}->{$member})); + $members->{$member} = 1; + } + } + return [ keys(%{$members}) ]; +} +foreach my $name (keys(%{$cf->{domain}})) { + next if ($name eq '-order'); + $cf->{domain}->{$name}->{members} = &expand_domain($name); +} + +# extend reverse/per-host domain specification with domain memberships +foreach my $host (keys(%{$cf->{host}})) { + next if ($host eq '-order'); + if (not defined($cf->{host}->{$host}->{domain})) { + $cf->{host}->{$host}->{domain} = []; + } + foreach my $domain (@{$cf->{domain}->{-order}}) { + next if ($domain eq '-order'); + foreach my $member (@{$cf->{domain}->{$domain}->{members}}) { + if ($member eq $host) { + if (not grep { $_ eq $domain } @{$cf->{host}->{$host}->{domain}}) { + push(@{$cf->{host}->{$host}->{domain}}, $domain); + } + } + } + } +} + +# expand group/domain references in access grant configuration +foreach my $grant (@{$cf->{grant}}) { + $grant->[0] = [ map { &expand_host($_) } @{$grant->[0]} ]; + $grant->[1] = [ map { &expand_user($_) } @{$grant->[1]} ]; + sub expand_host { + my ($host) = @_; + my $tag = ""; + if ($host =~ m|^(.+)(\[[^\]]+\])$|s) { + ($host, $tag) = ($1, $2); + } + if ($host =~ m|^~(.+)$|s) { + # regular expression match + $host = $1; + if ($host =~ m|^\@(.+)$|s) { + my $regex = $1; + my @hosts = (); + foreach my $domain (keys(%{$cf->{domain}})) { + next if ($domain eq '-open'); + if ($domain =~ m|$regex|s) { + foreach my $host (@{$cf->{domain}->{$domain}->{members}}) { + push(@hosts, $host.$tag); + } + } + } + die "no domains match regular expression \"$regex\"" + if (@hosts == 0); + return @hosts; + } + else { + my $regex = $host; + my @hosts = (); + foreach my $host (keys(%{$cf->{host}})) { + next if ($host eq '-open'); + if ($host =~ m|$regex|s) { + push(@hosts, $host.$tag); + } + } + die "no hosts match regular expression \"$regex\"" + if (@hosts == 0); + return @hosts; + } + } + else { + # plain text match + if ($host =~ m|^\@(.+)$|s) { + my $domain = $1; + die "unknown domain \"$domain\"" + if (not defined($cf->{domain}->{$domain})); + return map { $_.$tag } @{$cf->{domain}->{$domain}->{members}}; + } + else { + die "unknown host \"$host\"" + if (not defined($cf->{host}->{$host})); + return ($host.$tag); + } + } + } + sub expand_user { + my ($user) = @_; + my @user = (); + if ($user =~ m|^\@(.+)$|s) { + my $group = $1; + die "unknown community group \"$group\"" + if (not defined($cf->{group}->{$group})); + return @{$cf->{group}->{$group}->{members}}; + } + else { + die "unknown user \"$user\"" + if (not defined($cf->{user}->{$user})); + return ($user); + } + } +} + +# multiply out access control list +my $acl_by_user = {}; +my $acl_by_host = {}; +foreach my $grant (@{$cf->{grant}}) { + my ($hosts, $users) = ($grant->[0], $grant->[1]); + foreach my $host (@{$hosts}) { + foreach my $user (@{$users}) { + $acl_by_user->{$user} ||= {}; + $acl_by_user->{$user}->{$host} = 1; + $acl_by_host->{$host} ||= {}; + $acl_by_host->{$host}->{$user} = 1; + } + } +} + +# optional configuration debugging +#print Data::Dumper->Dump([$cf]); +#exit(0); + +## _________________________________________________________________________ +## +## Main Procedure +## _________________________________________________________________________ +## + +# operation: update Bash command line completion cache +my $rc = 0; +if ($opt->{update}) { + $rc = &do_update(); +} +if (@ARGV == 0) { + $rc = &do_cli(); +} +elsif (@ARGV == 1) { + $rc = &do_connect($ARGV[0]); +} +else { + die "invalid number of arguments"; +} +exit($rc); + +## _________________________________________________________________________ +## +## Utility Functions +## _________________________________________________________________________ +## + +# canonicalize filesystem path +sub path_canonicalize { + my ($path) = @_; + + $path =~ s|/{2,}|/|sg; + 1 while ($path =~ s|/\./||s); + 1 while ($path =~ s|/[^/]+/\.\./|/|s); + return $path; +} + +# encode a string into Base64 chunk +sub base64_encode { + my ($obj, $eol) = @_; + my $res = ''; + + $eol = "\n" unless defined($eol); + pos($obj) = 0; + while ($obj =~ m/(.{1,45})/gs) { + $res .= substr(pack('u', $1), 1); + chop($res); + } + $res =~ tr/` -_/AA-Za-z0-9+\//; + my $padding = (3 - length($obj) % 3) % 3; + $res =~ s/.{$padding}$/'=' x $padding/e if $padding; + $res =~ s/(.{1,64})/$1$eol/g if (length($eol) > 0); + return $res; +} + +# decode a string from a Base64 chunk +sub base64_decode { + local($^W) = 0; + my ($str) = @_; + my $res = ''; + + $str =~ tr|A-Za-z0-9+=/||cd; + die 'invalid length of Base64 chunk' if (length($str) % 4); + $str =~ s/=+$//; + $str =~ tr|A-Za-z0-9+/| -_|; + while ($str =~ m/(.{1,60})/gs) { + my $len = chr(32 + length($1)*3/4); + $res .= unpack('u', $len.$1); + } + return $res; +} + +sub text_fit { + my ($col, $str) = @_; + if (length($str) > $col) { + $str = substr($str, 0, $col); + } + return sprintf("%-${col}s", $str); +} + +sub text_trim { + my ($col, $str) = @_; + if (length($str) > $col) { + $str = substr($str, 0, $col); + } + return $str; +} + +## _________________________________________________________________________ +## +## Operation: Update Command Line Completion Cache +## _________________________________________________________________________ +## + +sub do_update { + my $cache = ''; + # command line options + foreach my $opt (keys(%getopt_spec)) { + if ($opt =~ m/^(.)\|(.*)$/s) { + $cache .= "option -$1\n"; + $cache .= "option --$2\n"; + } + } + # hostnames + foreach my $host (@{$cf->{host}->{-order}}) { + if ($acl_by_user->{$my->{user_name}}->{$host}) { + $cache .= "host ".$cf->{host}->{$host}->{hostname}."\n"; + } + } + my $io = new IO::File ">$my->{prog_complete}" + or die "cannot write to completion cache file \"$my->{prog_complete}\""; + $io->print($cache); + $io->close; + exit(0); +} + +## _________________________________________________________________________ +## +## User Interface: Command Line Interface (CLI) +## _________________________________________________________________________ +## + +sub do_cli { + my $user = $cf->{user}->{$my->{user_name}} + or die "unknown user \"$my->{user_name}\""; + + # FIXME + print " __ _ ___ \n" . + " / _\` |/ __| Welcome ".$user->{realname}." <".$user->{email}.">\n" . + "| (_| | (__ to the Cable & Wireless Auto Connect (AC) frontend\n" . + " \\__,_|\\___| shell running on server ac.de.cw.net\n" . + "\n" . + "In case of any questions or problems please contact:\n" . + "Internet Services, Ralf S. Engelschall \n" . + "\n"; + + # deferred loading of Term::ReadLine::Gnu + eval { + local $^W = 0; + use Term::ReadLine; + use Term::ReadLine::Gnu; + }; + + my $rl = new Term::ReadLine $my->{prog_name}; + my $out = $rl->OUT || \*STDOUT; + my $prompt = $my->{user_name}."@".$my->{prog_name}."> "; + my $input; + $rl->ornaments("md,me,,"); + $rl->MinLine(undef); + $rl->read_history($my->{prog_history}); + my $rla = $rl->Attribs; + $rla->{completion_function} = sub { + my ($text, $line, $start) = @_; + my @r = (); + if ($start == 0) { + @r = qw(info connect quit); + } + elsif ($line =~ m|^\s*i(n(f(o?)?)?)?\s+\S*$|s) { + @r = map { $cf->{host}->{$_}->{hostname} } + keys(%{$acl_by_user->{$my->{user_name}}}); + } + elsif ($line =~ m|^\s*c(o(n(n(e(ct?)?)?)?)?)?\s+\S*$|s) { + @r = map { $cf->{host}->{$_}->{hostname} } + keys(%{$acl_by_user->{$my->{user_name}}}); + } + return @r; + }; + while (defined ($input = $rl->readline($prompt))) { + if ($input =~ m|^\s*q(u(it?)?)?\s*$|s) { + last; + } + elsif ($input =~ m|^\s*i(n(f(o?)?)?)?|s) { + if ($input =~ m|^\s*\S+\s*$|s) { + my $rc = &do_info("*"); + } + elsif ($input =~ m|^\s*\S+\s+(\S+)\s*$|s) { + my $rc = &do_info($1); + } + else { + print $out $my->{prog_name}.":ERROR: invalid arguments on 'info' command\n"; + } + } + elsif ($input =~ m|^\s*c(o(n(n(e(ct?)?)?)?)?)?|s) { + if ($input =~ m|^\s*\S+\s*$|s) { + my $rc = &do_connect("*"); + } + elsif ($input =~ m|^\s*\S+\s+(\S+)\s*$|s) { + my $rc = &do_connect($1); + } + else { + print $out $my->{prog_name}.":ERROR: invalid arguments on 'connect' command\n"; + } + } + elsif ($input =~ m/^\s*\?\s*$/s) { + print $out + "info ......... output summary information\n" . + "connect ...... connect to a particular host\n" . + "quit .................... quit this session\n"; + } + elsif ($input eq '') { + # no operation + } + else { + print $out $my->{prog_name}.":ERROR: invalid command (press '?' for help)\n"; + } + $rl->addhistory($input) if ($input !~ m/^\S+$/); + } + print $out "\n" if (not defined($input)); + $rl->write_history($my->{prog_history}); + return 0; +} + +## _________________________________________________________________________ +## +## User Interface: Console User Interface (CUI) +## _________________________________________________________________________ +## + +sub do_cui { + my (@hosts) = @_; + + # deferred loading of Curses::UI + if (not defined($Curses::VERSION) and not defined($Curses::UI::VERSION)) { + eval { + local $^W = 0; + use Curses; + use Curses::UI; + }; + } + + # create the CUI root object + my $cui = new Curses::UI ( + -clear_on_exit => 0, + -debug => 0 + ); + my $W = {}; + + # determine width of screen + my $max_width = $cui->width(); + die "screen width smaller than 80 characters" if ($max_width < 80); + my $cols = [["p", 2], ["s", 22], ["p", 1], ["s", 13], ["p", 1], + ["s", 12], ["p", 1], ["s", 12], ["p", 1], ["s", 13], ["p", 2]]; + my $width = 0; + foreach my $col (@{$cols}) { + $width += $col->[1]; + } + my $i = 0; + while ($width < $max_width) { + while ($cols->[$i]->[0] eq 'p') { + $i = (($i + 1) % ($#{$cols}+1)); + } + $cols->[$i]->[1]++; + $width++; + $i = (($i + 1) % ($#{$cols}+1)); + } + my $fmt = ""; + foreach my $col (@{$cols}) { + if ($col->[0] eq 'p') { + $fmt .= (" " x ($col->[1])); + } + else { + $fmt .= "%-". $col->[1] . "s"; + } + } + + # determine host list + my $list_values = []; + my $list_labels = {}; + my $domains = $cf->{domain}->{-order}; + $i = 0; + foreach my $domain (@{$domains}) { + my $headed = 0; + my $hosts = $cf->{host}->{-order}; + foreach my $host (@{$hosts}) { + if ((grep { $_ eq $cf->{host}->{$host}->{hostname} } @hosts) == 1 + and $cf->{host}->{$host}->{domain}->[0] eq $domain) { + if (not $headed) { + push(@{$list_values}, "::$i"); + $list_labels->{"::$i"} = ""; + $i++; + push(@{$list_values}, "::$i"); + $list_labels->{"::$i"} = " ".$cf->{domain}->{$domain}->{description}.""; + $i++; + $headed = 1; + } + my $label = sprintf($fmt, + &text_trim($cols->[1]->[1], $cf->{host}->{$host}->{'hostname'}), + &text_trim($cols->[3]->[1], $cf->{host}->{$host}->{'purpose'}), + &text_trim($cols->[5]->[1], $cf->{host}->{$host}->{'system'}), + &text_trim($cols->[7]->[1], $cf->{host}->{$host}->{'platform'}), + &text_trim($cols->[9]->[1], $cf->{host}->{$host}->{'location'}), + $cf->{host}->{$host}->{'hostname'}); + push(@{$list_values}, $cf->{host}->{$host}); + $list_labels->{$cf->{host}->{$host}} = $label; + } + } + } + + # add main full-size window + $W->{main} = $cui->add( + 'main', 'Window', + -title => "", + -border => 0, + -padtop => 0, + -padbottom => 0, + -ipad => 0, + -width => -1, + -height => -1, + ); + + # add header line + my $header = sprintf($fmt, + "Hostname", "Purpose", "System", "Platform", "Location" + ); + $W->{header} = $W->{main}->add( + 'header', 'Label', + -y => 0, + -x => 0, + -width => -1, + -height => 1, + -border => 0, + -reverse => 1, + -paddingspaces => 1, + -text => $header, + ); + + # add scrolling list + $W->{list} = $W->{main}->add( + 'list', 'Listbox', + -y => 1, + -x => 0, + -width => $W->{main}->width, + -height => $W->{main}->height - 2, + -border => 0, + -values => $list_values, + -labels => $list_labels, + -multi => 0, + -title => "", + -vscrollbar => 0, + -htmltext => 1, + ); + + # add footer line + my $footer = sprintf($fmt, + "Hostname", "Purpose", "System", "Platform", "Location" + ); + $W->{footer} = $W->{main}->add( + 'footer', 'Label', + -y => -1, + -x => 0, + -width => -1, + -height => 1, + -text => $footer, + -border => 0, + -reverse => 1, + -paddingspaces => 1 + ); + + # overwrite Curses::UI::Listbox key bindings + # to provide skipped plain-text list items + $W->{list}->set_binding(sub { + my $w = shift; + $w->do_routine("option-prevpage"); + $w->layout_content; + while ($w->get_active_value() =~ m|^::|) { + $w->do_routine("option-next"); + } + }, KEY_PPAGE()); + $W->{list}->set_binding(sub { + my $w = shift; + $w->do_routine("option-nextpage"); + $w->layout_content; + while ($w->get_active_value() =~ m|^::|) { + $w->do_routine("option-prev"); + } + }, KEY_NPAGE()); + $W->{list}->set_binding(sub { + my $w = shift; + my $n = 0; + my $y = $w->{-ypos}; + return if ($y <= 0); + $y--, $n++; + $y--, $n++ while ($y > 0 and $w->{-values}->[$y] =~ m|^::|); + $y++, $n-- while ($w->{-values}->[$y] =~ m|^::|); + $w->do_routine("option-prev") while ($n-- > 0); + }, KEY_UP()); + $W->{list}->set_binding(sub { + my $w = shift; + my $n = 0; + my $y = $w->{-ypos}; + return if ($y >= $w->{-max_selected}); + $y++, $n++; + $y++, $n++ while ($y < $w->{-max_selected} and $w->{-values}->[$y] =~ m|^::|); + $y--, $n-- while ($w->{-values}->[$y] =~ m|^::|); + $w->do_routine("option-next") while ($n-- > 0); + }, KEY_DOWN()); + + # CUI termination indicator + my $cui_exit = 0; + + # terminate with "q" and LEFT + $cui->set_binding(sub { + $cui_exit = 2; + }, "q"); + $cui->set_binding(sub { + $cui_exit = 2; + }, KEY_LEFT()); + + # select entry on RETURN and RIGHT + $W->{list}->set_binding(sub { + my $w = shift; + $w->do_routine("option-select"); + $cui_exit = 1; + }, KEY_ENTER()); + $W->{list}->set_binding(sub { + my $w = shift; + $w->do_routine("option-select"); + $cui_exit = 1; + }, KEY_RIGHT()); + + # show info box on SPACE + $W->{list}->set_binding(sub { + my $w = shift; + my $host = $w->get_active_value() || return; + + my $lastlogin = "NOW"; + my $text = sprintf( + " Hostname: %s Purpose: %s \n" . + " Platform: %s Location: %s \n" . + " System: %s Last Login: %s ", + &text_fit(22, $host->{'hostname'}), + &text_fit(22, $host->{'purpose'}), + &text_fit(22, $host->{'platform'}), + &text_fit(22, $host->{'location'}), + &text_fit(22, $host->{'system'}), + &text_fit(22, $lastlogin)); + my $box = $W->{main}->add( + 'box', 'Dialog::Status', + -message => $text + ); + $box->draw; + $box->focus; + $W->{main}->delete('box'); + }, " "); + + # hack: inject DOWN and UP key sequence in order to get rid of + # special case when first entry is a skipped plain-text entry. + $W->{list}->process_bindings(KEY_DOWN()); + $W->{list}->process_bindings(KEY_UP()); + + # now perform the CUI main loop + $cui->focus(undef, 1); + $cui->draw; + $cui->do_one_event() until ($cui_exit); + + # determine result + my $host = ($cui_exit == 1 ? $W->{list}->get_active_value() : undef); + $host = $host->{hostname} if (defined($host) and ref($host)); + + # shutdown CUI + $cui->DESTROY; + undef $cui; + + # install program termination preparation sequence where stdout is + # redirected to get rid of the nasty (screen clearing) after-endwin() + # NCurses program termination cleanup sequence (which cannot be + # disabled from Perl as it looks). + END { + my $fd_stdout_new = POSIX::open("/dev/null", O_RDWR) || die; + my $fd_stdout_old = POSIX::dup(fileno(STDOUT)); + dup2($fd_stdout_new, fileno(STDOUT)); + } + + return $host; +} + +## _________________________________________________________________________ +## +## Operation: Display Host Information +## _________________________________________________________________________ +## + +sub do_info { + my ($hostname) = @_; + + # create hostname regex out of hostname pattern + my $re_hostname = $hostname; + $re_hostname =~ s|([.\$\@\%])|\\$1|sg; + $re_hostname =~ s|\*|.*|s; + $re_hostname =~ s|\?|.|s; + + # iterate over all hosts in configuration order + my $first = 1; + foreach my $host (@{$cf->{host}->{-order}}) { + my $host = $cf->{host}->{$host}; + if ($host->{hostname} =~ m/^.*${re_hostname}.*$/) { + # FIXME + my $lastlogin = "NOW"; + print "\n" if (not $first); + $first = 0; + printf(" Hostname: %s Purpose: %s\n" . + " Platform: %s Location: %s\n" . + " System: %s Last Login: %s\n", + &text_fit(22, $host->{'hostname'}), + &text_fit(22, $host->{'purpose'}), + &text_fit(22, $host->{'platform'}), + &text_fit(22, $host->{'location'}), + &text_fit(22, $host->{'system'}), + &text_fit(22, $lastlogin)); + } + } + return 0; +} + +## _________________________________________________________________________ +## +## Operation: Connect to a Host +## _________________________________________________________________________ +## + +# operation: connect to a host +sub do_connect { + my ($host) = @_; + + # expand hostname via full hostname list + my @hosts = map { $cf->{host}->{$_}->{hostname} } + keys(%{$acl_by_user->{$my->{user_name}}}); + my $re_host = $host; + $re_host =~ s|([.\$\@\%])|\\$1|sg; + $re_host =~ s|\*|.*|s; + $re_host =~ s|\?|.|s; + @hosts = grep { m/^.*${re_host}.*$/ } @hosts; + + my $rc = 0; + if (@hosts == 0) { + die "host \"".$host."\" not known"; + } + elsif (@hosts == 1) { + $rc = &do_connect_host($hosts[0]); + } + else { + my $hostname = &do_cui(@hosts); + if (defined($hostname)) { + $rc = &do_connect_host($hostname); + } + } + return $rc; +} + +sub do_connect_host { + my ($hostname) = @_; + + my $host = undef; + foreach my $h (keys(%{$cf->{host}})) { + next if ($h eq '-order'); + if ($cf->{host}->{$h}->{hostname} eq $hostname) { + $host = $cf->{host}->{$h}; + last; + } + } + + # FIXME + my $lastlogin = "NOW"; + + # FIXME? Really? + if ($opt->{verbose}) { + &do_info($host->{'hostname'}); + } + + # deferred loading of Term::ReadLine::Gnu + if (not defined($Expect::VERSION)) { + eval { + local $^W = 0; + use Expect; + }; + }; + + print "$my->{prog_name}: connection establishing\n"; + + # prepare script execution context + my $login = $host->{login}; + my $scr = $cf->{method}->{$login->[0]} + or die "invalid login method \"$login->[0]\""; + my $ctx = {}; + $ctx->{exp} = new Expect; + $ctx->{exp}->raw_pty(0); + $ctx->{timeout} = [ 10 ]; + $ctx->{level} = 0; + $ctx->{args_by_pos} = [ $login->[0] ]; + $ctx->{args_by_name} = {}; + $ctx->{backref} = [ '' ]; + $ctx->{spawned} = 0; + my $i = 1; + $ctx->{args_by_name}->{hostname} = $host->{hostname}; # special + foreach my $arg (@{$login}[1..$#{$login}]) { + if ($arg =~ m|^([a-zA-Z][a-zA-Z0-9_]*)=(.*)$|) { + $ctx->{args_by_name}->{$1} = $2; + } + else { + $ctx->{args_by_pos}->[$i++] = $arg; + } + }; + + # perform script execution + &execute_script($ctx, $scr); + + # shutdown script execution and destroy context + $ctx->{exp}->soft_close() if ($ctx->{spawned}); + print "\r"; # fix terminal cursor after scripting + undef $ctx->{exp}; + + print "$my->{prog_name}: connection droppped\n"; + + return 0; +} + +## _________________________________________________________________________ +## +## Operation: Execute Send/Expect Script +## _________________________________________________________________________ +## + +# expand an $X argument +sub expand_arg { + my ($ctx, $token) = @_; + $token =~ s/\#(\d+)/$ctx->{backref}->[$1] || ''/sge; + $token =~ s/\#\{(\d+)\}/$ctx->{backref}->[$1] || ''/sge; + + $token =~ s/\$(\d+)/$ctx->{args_by_pos}->[$1] || ''/sge; + $token =~ s/\$\{(\d+)\}/$ctx->{args_by_pos}->[$1] || ''/sge; + $token =~ s/\&\{([a-zA-Z][a-zA-Z0-9_]*)\}/$ctx->{args_by_name}->{$1} || ''/sge; + return $token; +} + +# execute send/expect script +sub execute_script { + my ($ctx, $scr) = @_; + + # execute all commands in sequence + foreach my $cmd (@{$scr}) { + if ($cmd->[0] eq 'system') { + # execute a command synchronous + my $shcmd = &expand_arg($ctx, $cmd->[1]); + my $rc = system($shcmd); + if ($rc != 0) { + die "failed to spawn sychronous command: \"$shcmd\": $!"; + } + } + elsif ($cmd->[0] eq 'spawn') { + # execute a command asynchronous + my $shcmd = &expand_arg($ctx, $cmd->[1]); + $ctx->{exp}->spawn($shcmd) + or die "failed to spawn asynchronous command \"$shcmd\": $!"; + $ctx->{spawned} = 1; + } + elsif ($cmd->[0] eq 'timeout') { + # set timeout (locally scoped) + my $timeout = &expand_arg($ctx, $cmd->[1]); + $timeout =~ m|^\d+$| + or die "invalid timeout \"$timeout\""; + $ctx->{timeout}->[-1] = $timeout; + } + elsif ($cmd->[0] eq 'send') { + # send a string + my $string = &expand_arg($ctx, $cmd->[1]); + $ctx->{exp}->send($string); + } + elsif ($cmd->[0] eq 'expect') { + # expect one ore more strings + + # open new scope + push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]); + $ctx->{level}++; + + # assemble expect list + my @expected = (); + sub expect_cb { + my ($exp, $ctx, $scr) = @_; + return '' if (@{$scr} == 0); # short circuiting + $ctx->{backref} = [ '', $exp->matchlist() ]; + return &::execute_script($ctx, $scr); # recursion(!) + }; + foreach my $rule (@{$cmd->[1]}) { + my $regex = &expand_arg($ctx, $rule->[0]); + my $expect = [ + "-re", $regex, + \&expect_cb, $ctx, $rule->[1] + ]; + push(@expected, $expect); + } + + # perform expect operation + $ctx->{exp}->expect($ctx->{timeout}->[-1], @expected); + + # close current scope + pop(@{$ctx->{timeout}}); + $ctx->{level}--; + } + elsif ($cmd->[0] eq 'sleep') { + # sleep for a while + my $num = &expand_arg($ctx, $cmd->[1]); + die "non-numeric argument to command 'sleep'" + if ($num !~ m/^(\d+|\d*\.\d+)$/); + select(undef, undef, undef, $num); + } + elsif ($cmd->[0] eq 'repeat') { + # indicate to Expect module the repeating of + # current 'expect' operation (see Expect manpage) + if ($ctx->{level} == 0) { + die "command 'repeat' not allowed at outmost level"; + } + return "exp_continue"; + } + elsif ($cmd->[0] eq 'interact') { + # enter interactive session until EOF + if ($ctx->{level} != 0) { + die "command 'interact' allowed at outmost level only"; + } + $ctx->{exp}->interact(); + } + elsif ($cmd->[0] eq 'if') { + my ($expr, $true, $false) = ($cmd->[1], $cmd->[2], $cmd->[3]); + my $rv = &execute_expr($ctx, $expr); + if ($rv) { + # open new scope + push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]); + $ctx->{level}++; + + # perform true clause + &execute_script($ctx, $true); + + # close current scope + pop(@{$ctx->{timeout}}); + $ctx->{level}--; + } + if (not $rv and defined($false)) { + # open new scope + push(@{$ctx->{timeout}}, $ctx->{timeout}->[-1]); + $ctx->{level}++; + + # perform true clause + &execute_script($ctx, $false); + + # close current scope + pop(@{$ctx->{timeout}}); + $ctx->{level}--; + } + } + else { + die "invalid command \"$cmd->[0]\""; + } + } + return ''; +} + +sub execute_expr { + my ($ctx, $expr) = @_; + + my $rv = ''; + if ($expr->[0] eq 'tok') { + # simple plain-text token + $rv = &expand_arg($ctx, $expr->[1]); + } + elsif ($expr->[0] =~ m/^op:([.*\/%+-]|==|!=|<|<=|>=|>)$/) { + # standard binary operator expression + my ($op, $e1, $e2) = ($1, $expr->[1], $expr->[2]); + $e1 = &execute_expr($ctx, $e1); + $e2 = &execute_expr($ctx, $e2); + if ( $op =~ m/^(==|!=|<|<=|>=|>)$/ + and ($e1 !~ m|^\d+$| or $e2 !~ m|^\d+$|)) { + # use string comparison operators except + # if both operands are really numerical + my %op_map = (qw(== eq != ne < lt <= le > gt >= ge)); + $op = $op_map{$op}; + } + eval "\$rv = (\$e1 $op \$e2);"; + } + elsif ($expr->[0] =~ m/^op:(=~)$/) { + # pattern matching binary operator expression + my ($op, $e1, $re) = ($1, $expr->[1], $expr->[2]); + $e1 = &execute_expr($ctx, $e1); + $re = &expand_arg($ctx, $re); + eval "\$rv = (\$e1 $op m/\$re/);"; + } + elsif ($expr->[0] =~ m/^op:([!-])$/) { + # standard unary operator expression + my ($op, $e1) = ($1, $expr->[1]); + $e1 = &execute_expr($ctx, $e1); + $op = '!' if ($op eq '-' and $e1 !~ m|^\d+$|); + $e1 = ($e1 =~ m|^\s*$|s ? 0 : 1) if ($op eq '!'); + eval "\$rv = ($op \$e1);"; + } + else { + die "invalid expression operator \"$expr->[0]\""; + } + return $rv; +} + Index: ossp-pkg/ac/ac.bash RCS File: /v/ossp/cvs/ossp-pkg/ac/ac.bash,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/ac/ac.bash,v' | diff -u /dev/null - -L'ossp-pkg/ac/ac.bash' 2>/dev/null --- ossp-pkg/ac/ac.bash +++ - 2024-05-10 19:23:53.902062978 +0200 @@ -0,0 +1,52 @@ +## +## OSSP ac -- Auto Connection +## Copyright (c) 2003 The OSSP Project +## Copyright (c) 2003 Cable & Wireless Germany +## Copyright (c) 2003 Ralf S. Engelschall +## +## This file is part of OSSP ac, a tool for automated login +## sessions which can be found at http://www.ossp.org/pkg/tool/ac/. +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version +## 2.0 of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this file; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +## USA, or contact The OSSP Project +## +## ac.bash: GNU Bash hooks (syntax: Bash 2.x) +## + +# command line completion hook function +ac_complete () { + # determine current context + local arg_pos="$COMP_CWORD" + local arg_cur="${COMP_WORDS[COMP_CWORD]}" + local arg_prev="${COMP_WORDS[COMP_CWORD-1]}" + + # initialize reply + COMPREPLY=() + + # create cache file if still not existing + if [ ! -f $HOME/.ac/.complete ]; then + command ac --update + fi + + # complete ac command line arguments + case "$arg_cur" in + -* ) COMPREPLY=($(compgen -W '$(egrep -- "^option" $HOME/.ac/.complete | sed -e "s;^[^ ]* ;;")' -- "$arg_cur")) ;; + * ) COMPREPLY=($(compgen -W '$(egrep -- "^host" $HOME/.ac/.complete | sed -e "s;^[^ ]* ;;")' -- "$arg_cur")) ;; + esac +} + +# hook function into ac command line completion sequence +complete -F ac_complete ac + Index: ossp-pkg/ac/ac.pod RCS File: /v/ossp/cvs/ossp-pkg/ac/ac.pod,v co -q -kk -p'1.1' '/v/ossp/cvs/ossp-pkg/ac/ac.pod,v' | diff -u /dev/null - -L'ossp-pkg/ac/ac.pod' 2>/dev/null --- ossp-pkg/ac/ac.pod +++ - 2024-05-10 19:23:53.904634527 +0200 @@ -0,0 +1,59 @@ +## +## OSSP ac -- Auto Connection +## Copyright (c) 2003 The OSSP Project +## Copyright (c) 2003 Cable & Wireless Germany +## Copyright (c) 2003 Ralf S. Engelschall +## +## This file is part of OSSP ac, a tool for automated login +## sessions which can be found at http://www.ossp.org/pkg/tool/ac/. +## +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either version +## 2.0 of the License, or (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this file; if not, write to the Free Software +## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +## USA, or contact The OSSP Project +## +## ac.pod: Unix manual page (syntax: Perl 5.x POD) +## + +=pod + +=head1 NAME + +B -- Auto Connection + +=head1 SYNOPSIS + +B +[B<-u>|B<--update>] +[B<-v>] + +=head1 DESCRIPTION + +=head1 HISTORY + +B, an interactive full-screen selection tool, was +implemented in 1997 by Ralf S. Engelschall. As a useful application and +demonstration of B, a small shell script wrapper named +F was implemented. Prompted by the requirement of automated +login sequences to Cable & Wireless Germany systems, in April 2003 the +idea of F was reimplemented from scratch in Perl. The result was +B 2.0. + +=head1 AUTHORS + + Ralf S. Engelschall + rse@engelschall.com + www.engelschall.com + +=cut +