#!/usr/local/bin/perl -w eval 'exec /usr/local/bin/perl -w -S $0 ${1+"$@"}' if 0; # not running under some shell # # 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 # - The following command: # a - append # c - change # d - delete # e - edit file # E - edit file - no questions asked # f - set/show filename # i - insert # p - print # P - print # q - quit # Q - no questions asked # r - read file # s - substitute regexp # w - write file # W - write file append # = - display line number # # # 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 # j - join # k - mark # l - "list" lines, show special chars # m - move # t - move/apend # 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 # - Save buffer on SIGHUP (meaningless on Windoz ?) # - discard NULS, chars after \n # - refuse to read non-ascii files # - Add BSD/GNU extentions # - add POD documentation # use Getopt::Std; # important globals $maxline = 0; # lines are numered internally starting at 1 $CurrentLineNum = 0; # default to before first line. $RememberedFilename = ""; # the default filename for writes, etc. $NeedToSave = 0; # buffer modified $UserHasBeenWarned = 0; # has the user been warned about attempt # to trash buffer/file? @lines = (); # buffer for file being edited. $command = ""; # single letter command entered by user @adrs = (); # array of line numbers (1 or 2) for command to operate on @args = (); # command arguments (filenames, search patterns...) # configuration vars $EXTENDED_MESSAGES = 0; # use useful error messages, not ed compatability messages # constants $NO_APPEND_MODE = 0; $NO_INSERT_MODE = 0; $INSERT_MODE = 1; $APPEND_MODE = 2; $QUESTIONS_MODE = 1; $NO_QUESTIONS_MODE = 0; $VERSION = "0.1"; # # Parse command line ... # # $opt_v = 0; $opt_d = ""; $SupressCounts = 0; unless (getopts('d:v')) { $EXTENDED_MESSAGES = 1; &edDie("Usgae: ed [-v] [-ddebugstring] [-] [filename]","\?",1) } if ($opt_v) { $EXTENDED_MESSAGES = 1; } print "@ARGV\n"; &edDie("Usgae: ed [-v] [-ddebugstring] [-] [filename]","\?",1) if ($#ARGV > 1); while ($_ = shift) { if (/-/) { $SupressCounts = 1; } else { $args[0] = $_; } } if ($EXTENDED_MESSAGES) { print STDERR "perl ed version $VERSION\n"; } &edEdit($NO_QUESTIONS_MODE,$NO_APPEND_MODE); # # parse commands and execute them... # while(<>) { 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, $maxline, $NeedToSave... # # navigation operations if ($command =~ /^$/) { &edSetCurrentLine; } elsif ($command =~ /^=$/) { &edPrintLineNum; # file operations } elsif ($command =~ /^[w]$/) { &edWrite($NO_APPEND_MODE); } elsif ($command =~ /^[W]$/) { &edWrite($APPEND_MODE); } elsif ($command =~ /^e$/) { &edEdit($QUESTIONS_MODE,$NO_INSERT_MODE); } elsif ($command =~ /^E$/) { &edEdit($NO_QUESTIONS_MODE,$NO_INSERT_MODE); } elsif ($command =~ /^r$/) { &edEdit($QUESTIONS_MODE,$INSERT_MODE); } elsif ($command =~ /^f$/) { &edFilename; # text manipulation commands } elsif ($command =~ /^[d]$/) { &edDelete; } elsif ($command =~ /^i$/) { &edInsert($INSERT_MODE); } elsif ($command =~ /^a$/) { &edInsert($APPEND_MODE); } elsif ($command =~ /^c$/) { &edDelete; $adrs[1] = undef; &edInsert($INSERT_MODE); } elsif ($command =~ /^s$/) { &edSubstitute; # misc commands } elsif ($command =~ /^[pP]$/) { &edPrint; } elsif ($command =~ /^q$/) { &edQuit($QUESTIONS_MODE); } elsif ($command =~ /^Q$/) { &edQuit($NO_QUESTIONS_MODE); } } else { edWarn("unrecognized command", "\?"); } } # # Print contents of requested lines # sub edPrint { $adrs[0] = $CurrentLineNum unless (defined($adrs[0])); $adrs[1] = $CurrentLineNum unless (defined($adrs[1])); unless ($adrs[1] >= $adrs[0]) { edWarn("Second address must be >= first","\?"); return; } if (defined($args[0])) { edWarn("Extra arguments detected","\?"); return; } for $i ($adrs[0]..$adrs[1]) { print "$lines[$i]"; } $CurrentLineNum = $adrs[1]; } # # 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])); $NumLines = $adrs[1]-$adrs[0]+1; unless ($adrs[1] >= $adrs[0]) { edWarn("Second address must be >= first","\?"); return; } 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 $lineno ($adrs[0]..$adrs[1]) { $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] = $CurrentLineNum unless (defined($adrs[1])); $NumLines = $adrs[1]-$adrs[0]+1; unless ($adrs[1] >= $adrs[0]) { edWarn("Second address must be >= first","\?"); return; } splice(@lines,$adrs[0],$NumLines); $NeedToSave = 1; $UserHasBeenWarned = 0; $maxline -= $NumLines; $CurrentLineNum = $adrs[0] >= $maxline ? $maxline : $adrs[0]+1; } # # 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]; } elsif (defined($RememberedFilename)) { print "$RememberedFilename\n"; } } # # Write requested lines # sub edWrite { my($AppendMode) = @_; my($Numlines,$filename,$chars); $chars = 0; $adrs[0] = 1 unless (defined($adrs[0])); $adrs[1] = $maxline unless (defined($adrs[1])); $filename = defined($args[0]) ? $args[0] : $RememberedFilename; if (defined($filename)) { $RememberedFilename = $filename; } else { edWarn("no default filename","\?"); } $NumLines = $adrs[1]-$adrs[0]+1; if ($AppendMode) { unless (open(FILE,">>$filename")) { edWarn("Unable to open $filename for writing: $!","\?"); return; } } else { unless (open(FILE,">$filename")) { edWarn("Unable to open $filename for writing: $!","\?"); return; } } for $line (@lines[$adrs[0]..$adrs[1]]) { print FILE $line; $chars += length($line); } $NeedToSave = 0; $UserHasBeenWarned = 0; print "$chars\n" unless ($SupressCounts); close(FILE); # 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_maxline,$tmp_chars); if ($InsertMode) { $adrs[0] = $maxline unless (defined($adrs[0])); 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; } } $filename = defined($args[0]) ? $args[0] : $RememberedFilename; $RememberedFilename = $filename; if ($filename eq "" ) { $CurrentLineNum = 0; $maxline = 0; return 1; } # suck the file into a temp array unless (open(SOURCE, "<$filename")) { edWarn ("unable to open $filename: $!","\?"); return 0; } @tmp_lines = (); $tmp_maxline = 0; $tmp_chars = 0; while() { push(@tmp_lines,$_); $tmp_chars += length; $tmp_maxline++; } close(SOURCE); # now that we've got it, figure out what to do with it if ($InsertMode) { if ($adrs[0] == $maxline) { push(@lines,@tmp_lines); $CurrentLineNum += $tmp_maxline; } elsif ($adrs[0] == 0) { shift(@lines); # get rid of undefined line unshift(@lines,@tmp_lines); unshift(@lines,undef); # put 0 line back $CurrentLineNum = $tmp_maxline; } else { shift(@lines); # get rid 0 line @tmp_lines2 = splice(@lines,0,$adrs[0]); unshift(@lines,@tmp_lines); unshift(@lines,@tmp_lines2); $CurrentLineNum += ($tmp_maxline); unshift(@lines,undef); # put 0 line back } $maxline += $tmp_maxline; $chars = $tmp_chars; $NeedToSave = 1; $UserHasBeenWarned = 0; } else { if ($NeedToSave) { if ($QuestionsMode) { unless ($UserHasBeenWarned) { $UserHasBeenWarned = 1; edWarn("file modified","\?"); return; } } } @lines = @tmp_lines; unshift(@lines,undef); # line 0 is not used $maxline = $tmp_maxline; $chars = $tmp_chars; $NeedToSave = 0; $UserHasBeenWarned = 0; $CurrentLineNum = $maxline; } unless ($lines[$maxline] =~ /\n$/) { $lines[$maxline] .= "\n"; print "Newline appended\n"; } print "$chars\n" unless ($SupressCounts); return 1; } # # Insert some text # sub edInsert { my($Mode) = @_; my(@tmp_lines,@tmp_lines2,$tmp_maxline,$tmp_chars); if (defined($args[0])) { edWarn("Extra arguments detected","\?"); return; } $adrs[0] = $CurrentLineNum unless (defined($adrs[0])); if (defined($args[1])) { edWarn("Too many addressses", "\?"); return; } if ($adrs[0] == 0 and ($Mode == $INSERT_MODE)) { edWarn("Can't insert before line 0","\?"); return; } # suck the text into a temp array @tmp_lines = (); $tmp_maxline = 0; $tmp_chars = 0; while(<>) { last if (/^\.$/); push(@tmp_lines,$_); $tmp_chars += length; $tmp_maxline++; } # now that we've got it, figure out what to do with it if ($Mode == $INSERT_MODE) { if ($adrs[0] == 1) { shift(@lines); # get rid of undefined line unshift(@lines,@tmp_lines); unshift(@lines,undef); # put 0 line back $CurrentLineNum = $tmp_maxline; } else { shift(@lines); # get rid 0 line @tmp_lines2 = splice(@lines,0,$adrs[0]-1); unshift(@lines,@tmp_lines); unshift(@lines,@tmp_lines2); $CurrentLineNum = $adrs[0] + $tmp_maxline - 1; unshift(@lines,undef); # put 0 line back } } elsif ($Mode == $APPEND_MODE) { if ($adrs[0] == 0) { shift(@lines); # get rid of undefined line unshift(@lines,@tmp_lines); unshift(@lines,undef); # put 0 line back $CurrentLineNum = $tmp_maxline; } else { shift(@lines); # get rid 0 line @tmp_lines2 = splice(@lines,0,$adrs[0]); unshift(@lines,@tmp_lines); unshift(@lines,@tmp_lines2); $CurrentLineNum = $adrs[0] + $tmp_maxline; unshift(@lines,undef); # put 0 line back } } $maxline += $tmp_maxline; if ($tmp_chars) { $NeedToSave = 1; $UserHasBeenWarned = 0; } return 1; } # # Print current line number # sub edPrintLineNum { my($adr); if (defined($args[0])) { edWarn("Extra arguments detected","\?"); return; } for $i (0..1) { $adr = $adrs[$i] if (defined($adrs[$i])); } $adr = $maxline unless (defined($adr)); 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 cuurent line # # Input: # $adrs[0] - the requested new current line # $maxline - the maxline # @lines - the buffer # # Side effects: # 1. $CurrentLineNum is set # 2. The new current line is printed. sub edSetCurrentLine { my($i,$adr); if (defined($args[0])) { edWarn("Extra arguments detected","\?"); return; } for $i (0..1) { $adr = $adrs[$i] if (defined($adrs[$i])); } 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 = (); @fields = (/^( ( ((\d+)|(\.)|(\$)|([\/\?]([^\/\?+-]+)[\/\?]?))? # num,.,$,pattern (([+-])?(\d+))? # [+]num | -num (([\+]+)|([-^]+))? # + | - ) # first expression (, # comma between adrs ((\d+)|(\.)|(\$)|([\/\?]([^\/\?+-]+)[\/\?]?))? # num,.,$,pattern (([+-])?(\d+))? # [+]num | -num (([\+]+)|([-^]+))? # + | - )? ([acdeEfipPqQrswW=])? # command char \s*(\S+)? # argument (filename, etc.) )$/x); return 0 if ($#fields == -1); # bad syntax if (defined($fields[27])) { $command = $fields[27]; } else { $command = ""; } $args[0] = $fields[28]; $adrs[0] = &CalculateLine(splice(@fields,1,13)); $adrs[1] = &CalculateLine(splice(@fields,1,13)); 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); $myline = undef; 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)) { $pattern = $searchadr; $pattern =~ s/\/$//; $pattern =~ s/^\///; if ($wholesearch =~ /^\/.*\/$/) { $myline = edSearchForward($pattern); } else { $myline = edSearchBackward($pattern); } # $command = ""; } } 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 # maxline - global # lines - global # # Return: # 0 - not found # >0 - line where first found # sub edSearchForward { my($pattern) = @_; my($line,$start); $start = $CurrentLineNum < $maxline ? $CurrentLineNum : 1; for $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 # maxline - 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; } # # Exit with error # # edDie($msg,$status) # sub edDie { my($useful_msg,$original_ed_msg,$status) = @_; if ($EXTENDED_MESSAGES) { printf STDERR "$useful_msg\n"; } else { print STDERR "$original_ed_msg\n"; } exit $status; } # # Exit with error # # edWarn($msg,$original_ed_msg) # sub edWarn { my($useful_msg,$original_ed_msg) = @_; if ($EXTENDED_MESSAGES) { printf STDERR "$useful_msg\n"; } else { print STDERR "$original_ed_msg\n"; } }