#!/usr/bin/perl
=begin metadata
Name: ed
Description: text editor
Author: George M Jones, gmj@infinet.com
License: gpl
=end metadata
=cut
# What: A perl version of Unix ed editor.
#
# Based on the v7 documentation using GNU ed version 0.2 as a reference.
#
# Currently implemented:
# - most addressing modes (".","$","#","/pat/","?pat?","[+-]#, etc.)
# - command parsing
# - regular expressions (using perl's built in regexps)
# - Both regular and extended (-v) error messages
#
# Why?:
# - Because ed is "always there" (or supposed to be anyways)
# - Because I violently dislike vi (on Unix) and NOTEPAD on WinDoze
# - Also see the "Perl Power Tools: The Unix Reconstruction Project"
# at http://language.perl.com/ppt/
# - Because working on this is more fun than Y2K testing !
#
# Who:
#
# George M Jones (gmj@infinet.com). Please send changes to me.
#
# When:
# Version 0.1
# - 06/09/99 - Created (note that non-Y2K compliant date !!!)
#
# How (Usage):
# ed [-ddebugflags] [-v] [-] [file]
#
# Irony: This was, of course, edited originaly with emacs...
#
# Commentary:
# The guiding principals of this implentation are:
# - Functionality
# - Understandability
#
# "perl coolness" was not a guiding principal, as the author is at best
# an accomplished perl user. If you like the program, use it.
# If you don't, don't.
#
# Legaleese:
# This program is released under the GNU Public License.
#
# Todo:
# - Implement the following commands from the v7 docs:
# g - global command
# k - mark
# u - undo
# v - global command "inVerted"
# x - get encryption key
# ! - shell command
#
# - Create regression test suite...test against "real" ed.
# - add a "-e" flag to allow it to be used in sed(1) like fashion.
# - add buffer size limitations for strict compatability
# - discard NULS, chars after \n
# - refuse to read non-ascii files
# - Add BSD/GNU extentions
#
use strict;
use Getopt::Std qw(getopts);
# important globals
my $CurrentLineNum = 0; # default to before first line.
my $RememberedFilename = undef; # the default filename for writes, etc.
my $NeedToSave = 0; # buffer modified
my $UserHasBeenWarned = 0; # warning given regarding modified buffer
my $Error = undef; # saved error string for h command
my $Prompt = undef; # saved prompt string for -p option
my $SupressCounts = 0; # byte counts are printed by default
my @lines; # buffer for file being edited.
my $command = ""; # single letter command entered by user
my @adrs; # 1 or 2 line numbers for commands to operate on
my @args; # command arguments (filenames, search patterns...)
my $EXTENDED_MESSAGES = 0;
# constants
my $NO_APPEND_MODE = 0;
my $NO_INSERT_MODE = 0;
my $INSERT_MODE = 1;
my $APPEND_MODE = 2;
my $QUESTIONS_MODE = 1;
my $NO_QUESTIONS_MODE = 0;
my $PRINT_NUM = 1;
my $PRINT_BIN = 2;
my $VERSION = '0.3';
my %ESC = (
7 => '\a',
8 => '\b',
9 => '\t',
10 => "\$\n",
11 => '\v',
12 => '\f',
13 => '\r',
92 => '\\\\',
);
my %WANT_FILE = (
'e' => 1,
'E' => 1,
'f' => 1,
'r' => 1,
'w' => 1,
'W' => 1,
);
$SIG{HUP} = sub {
if ($NeedToSave) {
my $fh;
if (open $fh, '>', 'ed.hup') {
shift @lines;
print {$fh} join('', @lines);
close $fh;
}
}
exit 1;
};
#
# Parse command line ...
#
#
my %opt;
getopts('d:p:sv', \%opt) or &Usage;
if (defined $opt{'p'}) {
$Prompt = $opt{'p'};
}
if ($opt{'v'}) {
$EXTENDED_MESSAGES = 1;
}
if ($opt{'s'}) {
$SupressCounts = 1;
}
&Usage if scalar(@ARGV) > 1;
my $fname = shift;
if (!defined($fname) || $fname eq '-') {
$SupressCounts = 1;
} else {
$args[0] = $fname;
}
if ($EXTENDED_MESSAGES) {
print STDERR "perl ed version $VERSION\n";
}
&edEdit($NO_QUESTIONS_MODE,$NO_APPEND_MODE);
#
# parse commands and execute them...
#
while (1) {
print $Prompt if defined $Prompt;
last unless ($_ = <>);
chomp;
if (&edParse) {
if ($opt{'d'} =~ /parse/) {
print "command: /$command/ ";
print "adrs[0]: /$adrs[0]/ " if defined($adrs[0]);
print "adrs[1]: /$adrs[1]/ " if defined($adrs[1]);
print "args[0]: /$args[0]/ " if defined($args[0]);
print "\n";
}
# sanity check addresses
if (defined($adrs[0])) {
if ($adrs[0] > maxline() || $adrs[0] < 0) {
edWarn('address out of range');
next;
}
}
if (defined($adrs[1])) {
if ($adrs[1] > maxline() || $adrs[1] < 0) {
edWarn('address out of range');
next;
}
}
if (defined($adrs[0]) and defined($adrs[1])) {
unless ($adrs[1] >= $adrs[0]) {
edWarn("Second address ($adrs[0]) must be >= first ($adrs[1])");
next;
}
}
#
# To add a new command:
# - add to the following if...elsif... list,
# - add to the command parsing regexp in edParse
# - add a new routine to implement the command
# - do command specific checks of adrs and args in the routine
# - have command operate on important globals (see top of file),
# such as @lines, $CurrentLineNum, $NeedToSave...
#
# navigation operations
if (!length($command)) {
&edSetCurrentLine;
} elsif ($command eq '=') {
&edPrintLineNum;
# file operations
} elsif ($command eq 'w') {
&edWrite($NO_APPEND_MODE);
} elsif ($command eq 'W') {
&edWrite($APPEND_MODE);
} elsif ($command eq 'e') {
&edEdit($QUESTIONS_MODE,$NO_INSERT_MODE);
} elsif ($command eq 'E') {
&edEdit($NO_QUESTIONS_MODE,$NO_INSERT_MODE);
} elsif ($command eq 'r') {
&edEdit($QUESTIONS_MODE,$INSERT_MODE);
} elsif ($command eq 'f') {
&edFilename;
# text manipulation commands
} elsif ($command eq 'd') {
&edDelete;
} elsif ($command eq 'i') {
&edInsert($INSERT_MODE);
} elsif ($command eq 'a') {
&edInsert($APPEND_MODE);
} elsif ($command eq 'c') {
&edDelete;
$adrs[1] = undef;
&edInsert($INSERT_MODE);
} elsif ($command eq 's') {
&edSubstitute;
# misc commands
} elsif ($command eq 'p') {
&edPrint;
} elsif ($command eq 'P') {
if (defined $Prompt) {
$Prompt = undef;
} else {
$Prompt = defined $opt{'p'} ? $opt{'p'} : '*';
}
} elsif ($command =~ /^q$/) {
&edQuit($QUESTIONS_MODE);
} elsif ($command =~ /^Q$/) {
&edQuit($NO_QUESTIONS_MODE);
} elsif ($command eq 'h') {
print("$Error\n") if (defined $Error);
} elsif ($command eq 'H') {
$EXTENDED_MESSAGES ^= 1;
if ($EXTENDED_MESSAGES && defined $Error) {
print "$Error\n";
}
} elsif ($command eq 'j') {
&edJoin;
} elsif ($command eq 'l') {
edPrint($PRINT_BIN);
} elsif ($command eq 'm') {
edMove(1);
} elsif ($command eq 'n') {
edPrint($PRINT_NUM);
} elsif ($command eq 't') {
&edMove;
}
} else {
edWarn('unrecognized command');
}
}
sub maxline {
my $n = $#lines;
if ($n < 0) {
$n = 0;
}
return $n;
}
#
# Print contents of requested lines
#
sub edPrint {
my $mode = shift;
$adrs[0] = $CurrentLineNum unless (defined($adrs[0]));
$adrs[1] = $adrs[0] unless (defined($adrs[1]));
if ($adrs[0] == 0 || $adrs[1] == 0) {
edWarn('invalid address');
return;
}
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
if ($mode == $PRINT_NUM) {
for my $i ($adrs[0] .. $adrs[1]) {
print $i, "\t", $lines[$i];
}
} elsif ($mode == $PRINT_BIN) {
for my $i ($adrs[0] .. $adrs[1]) {
print escape_line($i);
}
} else {
for my $i ($adrs[0] .. $adrs[1]) {
print $lines[$i];
}
}
$CurrentLineNum = $adrs[1];
}
sub escape_line {
my $idx = shift;
my @chars = unpack 'C*', $lines[$idx];
if (scalar(@chars) == 0) {
die 'internal error: unpack';
}
my $s = '';
foreach my $c (@chars) {
if (exists $ESC{$c}) {
$s .= $ESC{$c};
} elsif (chr($c) !~ m/[[:print:]]/) {
$s .= sprintf '\%03o', $c;
} else {
$s .= chr($c);
}
}
return $s;
}
# merge lines back into $lines[$adrs[0]]
sub edJoin {
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
if (defined($adrs[0]) && $adrs[0] == 0) {
edWarn('invalid address');
return;
}
if (defined($adrs[1]) && $adrs[1] == 0) {
edWarn('invalid address');
return;
}
if (!defined($adrs[0]) && !defined($adrs[1])) {
if ($CurrentLineNum == maxline()) {
edWarn('invalid address');
return;
}
$adrs[0] = $CurrentLineNum;
$adrs[1] = $CurrentLineNum + 1;
}
elsif (defined($adrs[0]) && !defined($adrs[1])) { # nop
return;
}
if ($adrs[0] == $adrs[1]) { # nop
return;
}
my $buf = $lines[$adrs[0]];
my $start = $adrs[0] + 1;
for my $i ($start .. $adrs[1]) {
chomp $buf;
$buf .= $lines[$i];
}
$lines[$adrs[0]] = $buf;
splice @lines, $start, $adrs[1] - $adrs[0];
$NeedToSave = 1;
$UserHasBeenWarned = 0;
}
sub edMove {
my $delete = shift;
my $start = $adrs[0];
my $end = $adrs[1];
unless (defined $start) {
$start = $end = $CurrentLineNum;
}
unless (defined $end) {
$end = $start;
}
if ($start == 0 || $end == 0) { # allowed for $dst only
edWarn('invalid address');
return;
}
my $dst = $args[0];
unless (defined $dst) {
$dst = $CurrentLineNum;
}
if ($delete) {
# move a line to itself
if ($start == $dst && $end == $dst) {
return;
}
# move a range into itself
if ($dst >= $start && $dst <= $end) {
edWarn('invalid destination');
return;
}
}
my $count = $end - $start + 1;
my @copy;
if ($count == 1) {
push @copy, $lines[$start];
} else {
@copy = @lines[$start .. $end];
}
if ($start > $dst && $end > $dst) {
splice(@lines, $start, $count) if $delete;
splice @lines, $dst + 1, 0, @copy;
} else {
# avoid $dst referring to the wrong line
splice @lines, $dst + 1, 0, @copy;
splice(@lines, $start, $count) if $delete;
}
$NeedToSave = 1;
$UserHasBeenWarned = 0;
}
#
# Perform text substitution
#
sub edSubstitute {
my($LastMatch,$char,$first,$middle,$last,$whole,$flags,$PrintLastLine);
$PrintLastLine = 0;
# parse args
$adrs[0] = $CurrentLineNum unless (defined($adrs[0]));
$adrs[1] = $CurrentLineNum unless (defined($adrs[1]));
unless (defined($args[0])) {
edWarn('Need a substitution string');
return;
}
# do wierdness to match semantics if last character
# is present or absent
$args[0] =~ /(.).*/;
$char = $1;
($whole,$first,$middle,$last,$flags) = ($args[0] =~ /(($char)[^"$char"]*($char)[^"$char"]*($char)?)([imsx]*)/);
if (defined($char) and defined($whole) and
($flags eq "") and (not defined($last))) {
$args[0] .= "$char";
$PrintLastLine = 1;
}
# do the search and substitution
$LastMatch = $CurrentLineNum;
for my $lineno ($adrs[0]..$adrs[1]) {
my $evalstring = "\$lines[\$lineno] =~ s$args[0]";
if (eval $evalstring) {
$LastMatch = $lineno;
$NeedToSave = 1;
$UserHasBeenWarned = 0;
}
}
$CurrentLineNum = $LastMatch;
print $lines[$LastMatch] if ($PrintLastLine);
}
#
# Delete requested lines
#
sub edDelete {
my($Numlines);
$adrs[0] = $CurrentLineNum unless (defined($adrs[0]));
$adrs[1] = $adrs[0] unless (defined($adrs[1]));
if ($adrs[0] == 0 || $adrs[1] == 0) {
edWarn('invalid address');
return;
}
my $NumLines = $adrs[1]-$adrs[0]+1;
splice(@lines,$adrs[0],$NumLines);
$NeedToSave = 1;
$UserHasBeenWarned = 0;
$CurrentLineNum = $adrs[0];
if ($CurrentLineNum > maxline()) {
$CurrentLineNum = maxline();
}
}
#
# Print or set filename
#
sub edFilename {
if (defined($adrs[0]) or defined($adrs[1])) {
edWarn("too many addresses for command: $#adrs (@adrs)");
return;
}
if (defined($args[0])) {
$RememberedFilename = $args[0];
return;
}
if (defined($RememberedFilename)) {
print "$RememberedFilename\n";
}
else {
edWarn('No current filename');
}
}
#
# Write requested lines
#
sub edWrite {
my($AppendMode) = @_;
my($fh, $filename, $chars);
$chars = 0;
if (!defined($adrs[0]) && !defined($adrs[1])) {
$adrs[0] = 1;
$adrs[1] = maxline();
} elsif (defined($adrs[0]) && !defined($adrs[1])) {
$adrs[1] = $adrs[0];
}
$filename = defined($args[0]) ? $args[0] : $RememberedFilename;
if (!defined($filename)) {
edWarn('No current filename');
return;
}
$RememberedFilename = $filename;
if ($AppendMode) {
unless (open $fh, '>>', $filename) {
warn "$filename: $!\n";
edWarn('cannot open output file');
return;
}
} else {
unless (open $fh, '>', $filename) {
warn "$filename: $!\n";
edWarn('cannot open output file');
return;
}
}
for my $line (@lines[$adrs[0]..$adrs[1]]) {
print {$fh} $line;
$chars += length($line);
}
$NeedToSave = 0;
$UserHasBeenWarned = 0;
print "$chars\n" unless ($SupressCounts);
close $fh;
# v7 docs say to chmod 666 the file ... we're not going to
# follow the docs *that* closely today (6/16/99 ---gmj)
}
#
# Read in the named file
#
# return:
# 0 - failure
# 1 - success
sub edEdit {
my($QuestionsMode,$InsertMode) = @_;
my(@tmp_lines,@tmp_lines2,$tmp_chars,$chars);
if ($InsertMode) {
if (!defined($adrs[0])) {
$adrs[0] = maxline();
}
if (defined($args[1])) {
edWarn('Too many addressses');
return;
}
} else {
if (defined($adrs[0]) or defined($adrs[1])) {
edWarn("too many addresses for command: $#adrs (@adrs)");
return;
}
}
my $filename = defined($args[0]) ? $args[0] : $RememberedFilename;
$RememberedFilename = $filename;
if ($filename eq "" ) {
$CurrentLineNum = 0;
return 1;
}
# suck the file into a temp array
if (-d $filename) {
warn "$filename: is a directory\n";
edWarn('cannot read input file');
return 0;
}
unless (open(SOURCE, '<', $filename)) {
warn "$filename: $!\n";
edWarn('cannot open input file');
return 0;
}
@tmp_lines = ();
$tmp_chars = 0;
$chars = 0;
while (<SOURCE>) {
push(@tmp_lines,$_);
$tmp_chars += length;
}
close(SOURCE);
# now that we've got it, figure out what to do with it
if ($InsertMode) {
if (maxline() != 0 && $adrs[0] == maxline()) {
push(@lines,@tmp_lines);
$CurrentLineNum = maxline();
} elsif ($adrs[0] == 0) {
shift(@lines); # get rid of undefined line
unshift(@lines,@tmp_lines);
unshift(@lines,undef); # put 0 line back
$CurrentLineNum = scalar(@tmp_lines);
} else {
shift(@lines); # get rid 0 line
@tmp_lines2 = splice(@lines,0,$adrs[0]);
unshift(@lines,@tmp_lines);
unshift(@lines,@tmp_lines2);
unshift(@lines,undef); # put 0 line back
$CurrentLineNum = $adrs[0] + scalar(@tmp_lines);
}
$chars = $tmp_chars;
$NeedToSave = 1;
$UserHasBeenWarned = 0;
} else {
if ($NeedToSave && $QuestionsMode && !$UserHasBeenWarned) {
$UserHasBeenWarned = 1;
edWarn('file modified');
return;
}
@lines = @tmp_lines;
unshift(@lines,undef); # line 0 is not used
$chars = $tmp_chars;
$NeedToSave = 0;
$UserHasBeenWarned = 0;
$CurrentLineNum = maxline();
}
if ($lines[maxline()] !~ /\n$/) {
$lines[maxline()] .= "\n";
$chars++;
print "Newline appended\n";
}
print "$chars\n" unless ($SupressCounts);
return 1;
}
#
# Insert some text
#
sub edInsert {
my($Mode) = @_;
my(@tmp_lines,@tmp_lines2,$tmp_chars);
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
$adrs[0] = $CurrentLineNum unless (defined($adrs[0]));
if (defined($adrs[1])) {
edWarn('Too many addresses');
return;
}
# suck the text into a temp array
@tmp_lines = ();
$tmp_chars = 0;
while (<>) {
last if (/^\.$/);
push(@tmp_lines,$_);
$tmp_chars += length;
}
# now that we've got it, figure out what to do with it
if ($Mode == $INSERT_MODE) {
shift @lines; # get rid of 0 line
if ($adrs[0] == 0 || $adrs[0] == 1) {
unshift(@lines,@tmp_lines);
$CurrentLineNum = scalar(@tmp_lines);
} else {
@tmp_lines2 = splice(@lines,0,$adrs[0]-1);
unshift(@lines,@tmp_lines);
unshift(@lines,@tmp_lines2);
$CurrentLineNum = $adrs[0] + scalar(@tmp_lines) - 1;
}
unshift @lines, undef; # put 0 line back
} elsif ($Mode == $APPEND_MODE) {
shift(@lines); # get rid of 0 line
if ($adrs[0] == 0) {
unshift(@lines,@tmp_lines);
$CurrentLineNum = scalar(@tmp_lines);
} else {
@tmp_lines2 = splice(@lines,0,$adrs[0]);
unshift(@lines,@tmp_lines);
unshift(@lines,@tmp_lines2);
$CurrentLineNum = $adrs[0] + scalar(@tmp_lines);
}
unshift(@lines,undef); # put 0 line back
}
if ($tmp_chars) {
$NeedToSave = 1;
$UserHasBeenWarned = 0;
}
return 1;
}
#
# Print current line number
#
sub edPrintLineNum {
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
my $adr = $adrs[1];
if (!defined($adr)) {
$adr = $adrs[0];
}
if (!defined($adr)) {
$adr = maxline();
}
print "$adr\n";
# v7 docs say this does not affect current line. GNU ed sets the line.
# We go with the v7 docs.
}
#
# Quit ed
#
sub edQuit {
my($QuestionMode) = @_;
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
if ($QuestionMode) {
if ($NeedToSave) {
unless ($UserHasBeenWarned) {
edWarn('Current buffer has been modified but not saved');
$UserHasBeenWarned = 1;
return;
}
}
}
exit 0;
}
#
# Set current line
#
# Input:
# $adrs[0] - the requested new current line
# @lines - the buffer
#
# Side effects:
# 1. $CurrentLineNum is set
# 2. The new current line is printed.
sub edSetCurrentLine {
if (defined($args[0])) {
edWarn('Extra arguments detected');
return;
}
my $adr = $adrs[1];
if (!defined($adr)) {
$adr = $adrs[0];
}
if (defined($adr)) {
# user gave us a line, go to it
if ($adr <= maxline() && $adr > 0 && maxline() != 0) {
$CurrentLineNum = $adr;
} else {
edWarn("requested line ($adr) out of range: 1-" . maxline());
return 0;
}
} else {
# simply increment the line
if ($CurrentLineNum < maxline()) {
$CurrentLineNum++;
} else {
edWarn('already at end of file');
return 0;
}
}
print "$lines[$CurrentLineNum]";
return 1;
}
#
# Parse the next command
#
# Input: $_
#
# Output:
# @adrs - the line number(s) of the lines on the input
# $command - single character command
#
# Return:
# 1 - success
# 0 - parse failure
#
sub edParse {
#
# Parse commands....and yes, this could be done a lot more elegantly
# with fancy regexps
#
@adrs = ();
my @fields =
(/^(
(
((\d+)|(\.)|(\$)|([\/\?]([^\/\?+-]+)[\/\?]?))?
# num,.,$,pattern
(([+-])?(\d+))? # [+]num | -num
(([\+]+)|([-^]+))? # + | -
) # first expression
(, # comma between adrs
((\d+)|(\.)|(\$)|([\/\?]([^\/\?+-]+)[\/\?]?))?
# num,.,$,pattern
(([+-])?(\d+))? # [+]num | -num
(([\+]+)|([-^]+))? # + | -
)?
([acdeEfhHijlmnpPqQrstwW=])? # command char
(\s*)(\S+)? # argument (filename, etc.)
)$/x);
return 0 if ($#fields == -1); # bad syntax
if (defined($fields[27])) {
$command = $fields[27];
} else {
$command = "";
}
my $space_sep = length $fields[28];
$args[0] = $fields[29];
$adrs[0] = &CalculateLine(splice(@fields,1,13));
$adrs[1] = &CalculateLine(splice(@fields,1,13));
if (defined($args[0]) && $WANT_FILE{$command} && !$space_sep) {
return 0;
}
return 1;
}
#
# Given a parsed address expression, calcuate & return the indicated line
#
sub CalculateLine {
my($expr1,
$adrexpr1,
$decimaladr,$dotadr,$dollaradr,
$wholesearch,$searchadr,
$offsetexpr,$offsetdir,$offsetammount,
$plusesorminusesexpr,$pluses,$minuses) = @_;
my($myline);
if ($opt{'d'} =~ /parse/) {
print "expr1=/$expr1/\n" if (defined($expr1));
print "decimaladr=/$decimaladr/\n" if (defined($decimaladr));
print "dotadr=/$dotadr/\n" if (defined($dotadr));
print "dollaradr=/$dollaradr/\n" if (defined($dollaradr));
print "wholesearch=/$wholesearch/\n" if (defined($wholesearch));
print "searchadr=/$searchadr/\n" if (defined($searchadr));
print "offsetexpr=/$offsetexpr/\n" if (defined($offsetexpr));
print "offsetdir=/$offsetdir/\n" if (defined($offsetdir));
print "offsetammount=/$offsetammount/\n"
if (defined($offsetammount));
print "plusesorminusesexpr=/$plusesorminusesexpr/\n"
if (defined($plusesorminusesexpr));
print "pluses=/$pluses/\n" if (defined($pluses));
print "minuses=/$minuses/\n" if (defined($minuses));
}
if (defined($expr1)) {
if (defined($decimaladr)) {
$myline = $decimaladr;
} elsif (defined($dotadr)) {
$myline = $CurrentLineNum;
} elsif (defined($dollaradr)) {
$myline = maxline();
} elsif (defined($searchadr)) {
my $pattern = $searchadr;
$pattern =~ s/[\?\/]$//;
$pattern =~ s/^[\?\/]//;
if ($wholesearch =~ /^\?/) {
$myline = edSearchBackward($pattern);
} else {
$myline = edSearchForward($pattern);
}
}
}
if (defined($offsetexpr)) {
$myline = $CurrentLineNum unless defined($myline);
if ($offsetdir =~ /^-$/) {
$myline -= $offsetammount;
} else {
$myline += $offsetammount;
}
}
if (defined($plusesorminusesexpr)) {
$myline = $CurrentLineNum unless defined($myline);
if (defined($pluses)) {
$myline += length($pluses);
} elsif (defined($minuses)) {
$myline -= length($minuses);
}
}
return $myline;
}
#
# Search forward for a pattern...wrap if not found
#
# Inputs:
# pattern - via argument
# CurrentLineNum - global
# lines - global
#
# Return:
# 0 - not found
# >0 - line where first found
#
sub edSearchForward {
my($pattern) = @_;
my $start = $CurrentLineNum < maxline() ? $CurrentLineNum : 1;
for my $line ($start .. maxline() , 1 .. $CurrentLineNum) {
if ($lines[$line] =~ /$pattern/) {
return $line;
}
}
return 0;
}
#
# Search backward for a pattern...wrap if not found
#
# Inputs:
# pattern - via argument
# CurrentLineNum - global
# lines - global
#
# Return:
# 0 - not found
# >0 - line where first found
#
sub edSearchBackward {
my($pattern) = @_;
my($line,$start);
# brute force search...
$start = $CurrentLineNum > 1 ? $CurrentLineNum - 1 : maxline();
$line = $start;
while ($line > 0) {
if ($lines[$line] =~ /$pattern/) {
return $line;
}
$line--;
}
$line = maxline();
while ($line >= $CurrentLineNum) {
if ($lines[$line] =~ /$pattern/) {
return $line;
}
$line--;
}
return 0;
}
#
# Print usage and exit
#
# Usage()
#
sub Usage {
die "Usage: ed [-p prompt] [-ddebugstring] [-sv] [file]\n";
}
#
# Print error and save it
#
# edWarn($msg)
#
sub edWarn {
my $msg = shift;
$Error = $msg;
print "?\n";
if ($EXTENDED_MESSAGES) {
print "$msg\n";
}
}
__END__
=encoding utf8
=head1 NAME
ed - text editor
=head1 SYNOPSIS
ed [-p prompt] [-ddebugstring] [-sv] [file]
=head1 DESCRIPTION
ed initially reads its input file into a buffer.
If no file argument is provided the buffer will be empty.
Commands are then entered to display, modify and save the buffer.
Line numbers within the buffer are referred to as addresses.
Address 1 is the first line in the buffer; address 0 is invalid.
ed keeps track of the current line selected in the buffer.
Commands for modifying the buffer can be entered without an explicit
address; the current line will be processed.
Entering a bare address such as "2" first resets the current
line pointer, then prints the line.
Commands are denoted by a single letter.
The "p" command prints one or more lines.
An address can precede a command, so "2p" first resets the current
line pointer then prints the line.
This is the same result as for entering the bare address "2";
however, print is explicit.
A command may operate on a range of addresses at once.
An address range is entered as two numbers separated by a comma, e.g. "1,10".
The numbers are included as the first and last number of the range.
So "1,10" spans from line 1 to 10.
A range is then used as a command prefix, e.g. "1,2p" will print 2 lines.
Addressing a line outside the scope of the buffer results in an error.
The commands "a", "c" and "i" allow text to be entered into the buffer.
Text input is terminated by a line containing the single character ".".
If an error occurs ed will print "?".
The "h" command fetches the saved error message and prints it.
=head2 OPTIONS
The following options are available:
=over 4
=item -ddebugstring
Print debugging output. debugstring may be set to "parse".
=item -p STRING
Use the specified STRING as a command prompt.
By default no prompt is displayed.
=item -s
Suppress byte counts
=item -v
Print full error messages; equivalent to the "H" command
=back
=head2 EDITOR COMMANDS
=over 4
=item a
Append text
=item c
Change text
=item d
Delete text
=item E FILE
Forced "e" command; suppress warning prompt
=item e FILE
Load and edit the named FILE argument
=item f [FILE]
Show/set a filename
=item H
Toggle help mode; this causes descriptive errors to be displayed
=item h
Display last error
=item i
Insert text
=item j
Join a range of lines into a single line
=item l
Print lines with escape sequences for non-printable characters
=item m
Move a range of lines to a new address
=item n
Print from buffer with line number prefix
=item P
Toggle command prompt mode.
A string provided by -p will be used; otherwise, the default is '*'
=item p
Print from buffer
=item Q
Forced "q" command; suppress warning prompt
=item q
Quit program
=item r FILE
Read named FILE into buffer
=item s///
Substitute text with a regular expression
=item t
Copy a range of lines to a destination address
=item W [FILE]
Write buffer to file in append mode
=item w [FILE]
Write buffer to file
=item =
Display current line number
=back
=head1 AUTHOR
Written by George M Jones
=cut