7 cled - Command Line-Editor - Edit text file lines by commands directly from shell
11 cled [<OPTIONS>] <EXPR> [<EXPR> [...]] [-- <FILE> [<FILE> [...]]]
15 Each EXPR expression consist of an optional SELECTOR term and a command
16 and variable number of arguments depending on the command
17 (see COMMANDS section), like:
19 <SELECTOR>[..<SELECTOR>] <COMMAD> [<ARG> [<ARG> [...]]]
21 You may narrow the COMMAND's effect by a SELECTOR term.
22 A SELECTOR selects either one line or a closed range of lines.
23 For ranges, put C<..> (double dot) between SELECTORs.
24 A SELECTOR may be a regexp, ie. C</PATTERN/[MODIFIERS]>,
25 or a line number, or the literal word C<last> which means the last line.
28 [ /<REGEXP>/[<MODIFIERS>][[+ | -]<OFFSET>] | <LINE> | last[-<OFFSET>] ]
34 Select line 10 and all subsequent lines down to the first one matching
35 to case-insensitive "lorem".
39 Select the lines between /begin/ and /end/ inclusively.
43 Select the lines between /begin/ and /end/ exclusively.
45 The 2nd SELECTOR does not get tested on the same line as the 1st
47 So you can select at least a 2-lines long range by eg. C</start/../stop/>.
48 If the 2nd SELECTOR is REGEXP and it does not match any lines, then
49 practically it's an open-ended range, so the rest of the file is
51 Line numbers are indexed from 0.
53 REGEXP and the C<last> SELECTOR have an optional OFFSET: eg. C</begin/+2>
54 selects the 2nd line following a line matching to /begin/. The C<last>
55 SELECTOR obviously supports only negative OFFSETs.
57 An EXPR expression can be a group denoted by brackets,
58 in which there are subexpressions like EXPR.
59 This way you can do multiple COMMAND commands, all when the common
60 SELECTOR matches to the line in order:
62 /^d/ [ ltrim prepend " " ]
64 Which removes all leading whitespace, if any, from lines starting with
65 "d" and inserts a single space at the beginning of all of them.
66 See COMMANDS below for all the supported editor commands.
68 Currently 3 type of brackets are supported: C<[ ... ]> square,
69 C<{ ... }> curly, and C<( ... )> parenthesis. Use the square one to
79 May specify multiple times.
80 Files are edited in-place by default, by persisting their Inode,
81 ie. buffer the output data and write to the original input file
82 when it's all read up.
83 If --output option(s) is (are) given, then the file(s) won't be
84 modified in-place, rather than saved in output file(s).
85 If not given any --file, works on STDIN and print to STDOUT.
87 =item -o, --output PATH
89 File to save modified data into.
90 May specify multiple times.
91 If less --output parameters given than --file, then the input files
92 without a corresponding output file will be edited in-place.
96 Prompt for confirmation for each selected line.
97 If a readline module is installed then you may do further changes to the
99 Term::ReadLine::Gnu(3pm) module is recommended.
101 Press Ctrl-J to insert newline (LF) at the cursor position,
102 as it's not added automaticaly to the end of line.
104 If no readline module available, press only a single Enter to accept
105 changes, and Ctrl-C to revert to the original line, or type in new
106 content and press Enter to replace the promped line (newline is added
107 to the end in this case).
108 Additionally an inverse space char at the end of line indicates if the last
109 line is not terminated by a newline.
113 Print edited lines to STDERR.
114 Prefixed with line number if option C<-l> is given.
115 A line is edited if it's selected by any SELECTOR and not reverted
116 thereafter at the interactive prompt.
120 Show line numbers in verbose mode.
128 use Getopt
::Long qw
/:config no_ignore_case bundling pass_through/;
129 use feature qw
/switch/;
130 no if ($] >= 5.018), 'warnings' => 'experimental::smartmatch';
132 # Term::ReadLine::Gnu is recommended
133 $readline_support = eval q{ use Term::ReadLine; 1; };
136 $0 =~ s/.*\/([^\/]+)$/$1/;
143 # Getopt::Long(3perl)
144 # When configured for bundling, single-character options are matched
145 # case sensitive while long options are matched case insensitive.
147 $SIG{'__WARN__'} = sub { warn "$0: $_[0]"; };
149 'c|confirm' => \
$OptConfirm,
150 'f|file=s@' => \
@Files,
151 'help|?' => sub{ pod2usage
(-exitval
=>0, -verbose
=>99); },
152 'o|output=s@' => \
@Output,
153 'v|verbose' => \
$OptVerbose,
154 'l' => \
$OptVerboseLnum,
156 delete $SIG{'__WARN__'};
159 sub readline_insert_lf
161 my $pos = $readline->Attribs->{'point'};
162 my $buf = $readline->Attribs->{'line_buffer'};
163 $readline->Attribs->{'line_buffer'} = substr($buf, 0, $pos)."\n".substr($buf, $pos);
164 $readline->Attribs->{'point'} += 1;
167 if($readline_support)
169 $readline = Term
::ReadLine
->new('cled');
170 $readline->ornaments(0);
171 $readline->add_defun('insert_lf', \
&readline_insert_lf
);
172 $readline->bind_keyseq('\\C-j', 'insert_lf');
173 $readline->variable_bind('echo-control-characters', 0);
179 return ${$state->{'line_ref'}} =~ /(?$_[1])$_[0]/;
184 return $state->{'line_num'} == $_[0];
186 sub select_line_num_cmp
189 return $state->{'line_num'} <=> $_[0];
202 my $selector_expr = shift;
204 my ($match_expr, $offset) = $selector_expr =~ /^(.+?)([+-]\d+|)$/;
209 $ref->{'sub'} = \
&select_line_eq
;
210 $ref->{'relative_sub'} = \
&select_line_num_cmp
;
211 $ref->{'arg'} = [int $match_expr];
215 $ref->{'sub'} = \
&select_never
;
217 when(m{^/(.*)/([[:alpha:]]*)$})
219 my ($pattern, $modifiers) = ($1, $2);
220 eval { "" =~ /(?$modifiers)$pattern/; 1; };
221 die "$0: $selector_expr: $@" if $@
;
222 $ref->{'sub'} = \
&select_by_match
;
223 $ref->{'arg'} = [$pattern, $modifiers];
227 if($selector_expr eq '')
229 $ref->{'sub'} = \
&select_always
;
233 die "$0: should not happen.";
237 $ref->{'offset'} = int($offset || 0);
247 =item s/<PATTERN>/<REPLACEMENT>/[<MODIFIERS>]
249 Regexp substitution. Works just like in perl(1). See perlre(1).
254 # define how many arguments each edit_* subroutine expects from the CLI
255 # by putting eg. $NumberOfCliArgs{'xyz'}=2 before "sub edit_xyz".
256 # Omit if does not need any CLI argument.
257 %NumberOfCliArgs = ();
263 Edit selected lines interactively by a readline interface.
264 See --confirm option in OPTIONS section for details.
271 $state->{'confirm'} = 1;
278 Delete matching line(s).
285 ${$state->{'line_ref'}} = '';
290 =item ltrim, rtrim, trim
292 Remove leading (ltrim), trailing (rtrim), or leading and trailing (trim) whitespace from the line.
293 End-of-line char (LF, \n) is preserved.
305 ${$state->{'line_ref'}} =~ s/^\s*//;
311 ${$state->{'line_ref'}} =~ /(\n)$/ and $eol = $1;
312 ${$state->{'line_ref'}} =~ s/\s*$//;
313 ${$state->{'line_ref'}} .= $eol;
318 =item replace STR1 STR2
320 Replace all STR1 to STR2.
324 $NumberOfCliArgs{'replace'} = 2;
328 ${$state->{'line_ref'}} =~ s/\Q$_[0]\E/$_[1]/g;
333 =item replaceword STR1 STR2
335 Replace whole word STR1 to STR2.
339 $NumberOfCliArgs{'replaceword'} = 2;
343 ${$state->{'line_ref'}} =~ s/\b\Q$_[0]\E\b/$_[1]/g;
348 =item replaceline STR
350 Replace the whole line to STR.
354 $NumberOfCliArgs{'replaceline'} = 1;
359 ${$state->{'line_ref'}} =~ /(\n)$/ and $eol = $1;
360 ${$state->{'line_ref'}} = $_[0];
361 ${$state->{'line_ref'}} .= $eol;
368 Prepend STR to the line.
372 $NumberOfCliArgs{'prepend'} = 1;
376 ${$state->{'line_ref'}} = $_[0] . ${$state->{'line_ref'}};
383 Insert STR as a whole line before the matching line(s).
384 Line numbering is preserved as there was not an inserted line,
385 ie. line numbers are not incremented.
389 $NumberOfCliArgs{'insertline'} = 1;
393 ${$state->{'line_ref'}} = $_[0] . "\n" . ${$state->{'line_ref'}};
398 =item insertfile PATH
400 Insert the content of PATH file before the matching line(s).
401 The last line of PATH file will be separated from the matched line by a
402 newline (LF) either way.
403 Line numbering is preserved as described above.
407 $NumberOfCliArgs{'insertfile'} = 1;
411 open my $fh, '<', $_[0] or die "$_[0]: $!\n";
413 my $filecontent = <$fh>;
415 my $eol = $filecontent =~ /\n$/ ?
'' : "\n";
416 ${$state->{'line_ref'}} = $filecontent . $eol . ${$state->{'line_ref'}};
423 Append STR to the line.
427 $NumberOfCliArgs{'append'} = 1;
432 ${$state->{'line_ref'}} =~ s/(\n)$// and $eol = $1;
433 ${$state->{'line_ref'}} .= $_[0];
434 ${$state->{'line_ref'}} .= $eol;
441 Append STR as a whole line to the matching line(s).
442 Line numbering is preserved as described above.
446 $NumberOfCliArgs{'appendline'} = 1;
450 ${$state->{'line_ref'}} .= $_[0] . "\n";
465 sub signal_int_handler
472 @group_selectors = ();
487 if(my ($selector_start, $selector_stop) = $ARGV[0] =~ m{^(/.*/[[:alpha:]]*(?:[+-]\d+|)|\d+)\.\.(/.*/[[:alpha:]]*(?:[+-]\d+|)|\d+|last(?:-\d+|))$})
489 $selector->{'repr'} = $ARGV[0];
490 $selector->{'start'} = {};
491 add_selector
$selector_start, $selector->{'start'};
492 $selector->{'stop'} = {};
493 add_selector
$selector_stop, $selector->{'stop'};
496 elsif(my ($selector_expr) = $ARGV[0] =~ m{^(/.*/[[:alpha:]]*(?:[+-]\d+|)|\d+)$})
498 # FIXME: make it possible to select the last line (single selector)
499 # TODO: last-N to select the last but Nth line (buffering required)
500 # FIXME: fix /asd/-1..5 style selectors
501 $selector->{'repr'} = $ARGV[0];
502 $selector->{'single'} = {};
503 add_selector
$selector_expr, $selector->{'single'};
507 if($ARGV[0] ~~ ['[', '(', '{'])
511 $selector->{'repr'} = '';
512 $selector->{'single'} = {};
513 add_selector
'', $selector->{'single'};
515 push @group_selectors, $selector;
520 if($ARGV[0] ~~ [']', ')', '}'])
522 die "$0: selector without a command: $selector->{'repr'}\n" if %$selector;
523 pop @group_selectors or die "$0: too many closing brackets\n";
528 my $editcmd = shift @ARGV or die "$0: missing command\n";
534 my @group_selectors_copy;
535 for my $sel_ref (@group_selectors)
538 push @group_selectors_copy, \
%sel;
540 push @
{$ThisCmd->{'selectors'}}, @group_selectors_copy;
544 push @
{$ThisCmd->{'selectors'}}, $selector;
547 when(m{^(s/.*/.*/.*)$})
549 $ThisCmd->{'editor'} = {'sub' => \
&cled_eval
, 'arg' => [sprintf('${$state->{"line_ref"}} =~ %s',$1)],};
553 my $edit_sub = sprintf 'edit_%s', $editcmd;
554 if(not exists &$edit_sub)
556 die "$0: unknown editor command or selector: $editcmd\n";
560 my $num_args = $NumberOfCliArgs{$editcmd};
563 @cli_args = @ARGV[0..$num_args-1];
564 shift @ARGV for 0..$num_args-1;
567 $ThisCmd->{'editor'} = {'name' => $editcmd, 'sub' => \
&{$edit_sub}, 'arg' => \
@cli_args};
571 $ThisCmd->{'n'} = scalar @Commands;
572 push @Commands, $ThisCmd;
575 undef @group_selectors;
576 #warn Dumper \@Commands;
579 # if no readline support and --confirm requested or any of the editor commands is 'edit'
580 if(not $readline_support and ($OptConfirm or grep {$_->{'editor'}->{'sub'} eq \
&edit_edit
} @Commands))
582 open $terminal_fh, '<', '/dev/tty' or die "$0: /dev/tty: $!\n";
587 sub filehandler_write
592 if($event eq 'append')
594 return print {$meta->{'fh'}} $data;
599 sub buffered_overwrite
608 $meta->{'buffer'} = '';
612 $meta->{'buffer'} .= $data;
616 print {$meta->{'fh'}} $meta->{'buffer'} or return 0;
617 truncate($meta->{'fh'}, tell $meta->{'fh'}) or return 0;
626 my $commands_ref = shift;
627 my @commands = @
$commands_ref;
628 my $output_cb = shift;
629 my $cb_metadata = shift;
632 # find out the maximum backtrack distance
633 # to set the buffer size which holds the lines' state
634 # which have not been decided to edit yet
635 # due to negative offsets
638 my $largest_negative_offset = 0;
639 for my $cmd (@commands)
641 for my $selector (@
{$cmd->{'selectors'}})
643 $largest_negative_offset = $selector->{'start'}->{'offset'} if $selector->{'start'}->{'offset'} < $largest_negative_offset;
644 $largest_negative_offset = $selector->{'stop'}->{'offset'} if $selector->{'stop'}->{'offset'} < $largest_negative_offset;
648 $output_cb->('start', undef, $cb_metadata);
650 while(my $line = <$fh>)
652 my %buffered_line = (
653 'data_before' => $line,
656 'line_ref' => \
$line,
662 for my $cmd (@commands)
666 for my $selector (@
{$cmd->{'selectors'}})
668 if(exists $selector->{'single'})
670 $selected = $selector->{'single'}->{'sub'}->($buffered_line{'state'}, @
{$selector->{'single'}->{'arg'}});
672 elsif(exists $selector->{'start'} and exists $selector->{'stop'})
674 # this is a range selector, let's maintain its state.
675 $selected = $selector->{'matched'};
677 if(defined $selector->{'will_start_after'})
679 $selector->{'will_start_after'} -= 1;
683 my $matched = $selector->{'start'}->{'sub'}->($buffered_line{'state'}, @
{$selector->{'start'}->{'arg'}});
686 my $offset = $selector->{'start'}->{'offset'};
689 $linebuffer[$_]->{'editors'}->[$cmd->{'n'}]->{'selected'} = 1 for $offset..-1;
690 $selector->{'matched'} = 1;
695 $selector->{'will_start_after'} = $offset;
699 if(defined $selector->{'will_start_after'} and $selector->{'will_start_after'} == 0)
701 $selector->{'matched'} = 1;
703 delete $selector->{'will_start_after'};
706 if(defined $selector->{'will_stop_after'})
708 $selector->{'will_stop_after'} -= 1;
712 # check if the range is being closed in this line.
713 my $match_stop = $selector->{'stop'}->{'sub'}->($buffered_line{'state'}, @
{$selector->{'stop'}->{'arg'}});
716 my $offset = $selector->{'stop'}->{'offset'};
719 $linebuffer[$_]->{'editors'}->[$cmd->{'n'}]->{'selected'} = 0 for (($offset+1)..-1);
720 $selector->{'matched'} = 0;
725 $selector->{'will_stop_after'} = $offset;
729 if(defined $selector->{'will_stop_after'} and $selector->{'will_stop_after'} == 0)
731 $selector->{'matched'} = 0;
732 delete $selector->{'will_stop_after'};
735 if(defined $selector->{'stop'}->{'relative_sub'})
737 my $cmp = $selector->{'stop'}->{'relative_sub'}->($buffered_line{'state'}, @
{$selector->{'stop'}->{'arg'}});
740 # this range is already closed because we have passed by the line number.
747 die "$0: should not happen.";
750 last if not $selected;
753 push @
{$buffered_line{'editors'}}, {
754 'selected' => $selected,
755 'editor' => $cmd->{'editor'},
759 push @linebuffer, \
%buffered_line;
761 process_line_buffer
(\
@linebuffer, $largest_negative_offset, $output_cb, $cb_metadata);
766 process_line_buffer
(\
@linebuffer, 0, $output_cb, $cb_metadata);
768 $output_cb->('finish', undef, $cb_metadata) or return 0;
773 sub process_line_buffer
775 my $linebuffer_ref = shift;
776 my $largest_negative_offset = shift;
777 my $output_cb = shift;
778 my $cb_metadata = shift;
780 while(scalar @
$linebuffer_ref > -$largest_negative_offset)
782 my $processing_line = shift $linebuffer_ref;
784 # perform all edit commands on this line
785 for my $editor (@
{$processing_line->{'editors'}})
787 # perform this edit command if any group selector or the command's own selector matched
788 if($editor->{'selected'})
790 $processing_line->{'selected'} = 1;
791 $editor->{'editor'}->{'sub'}->($processing_line->{'state'}, @
{$editor->{'editor'}->{'arg'}});
792 $processing_line->{'edited'} = 1;
796 # prompt user for confirmation if needed
797 my $do_confirm = $OptConfirm;
798 $do_confirm = $processing_line->{'state'}->{'confirm'} if defined $processing_line->{'state'}->{'confirm'};
800 my $before = $processing_line->{'data_before'};
801 my $line = ${$processing_line->{'state'}->{'line_ref'}};
802 my $lnum = $processing_line->{'state'}->{'line_num'};
804 if($processing_line->{'selected'} and $do_confirm)
807 print STDERR
"$lnum: $before";
808 print STDERR
"\n" unless $before =~ /\n$/;
809 my $prompt = "$lnum> ";
810 my $old_sigaction = {};
813 if($readline_support)
815 my $startup_hook = $readline->Attribs->{'startup_hook'};
816 # Perl's signal handlers (%SIG) don't work in readline,
817 # so setup a low-level signal handler to intercept Ctrl-C.
818 sigaction SIGINT
, POSIX
::SigAction
->new(sub {
819 # readline is interrupted so it can not restore the startup_hook
820 # which was modified to "preput" the initial data in the rl buffer.
821 $readline->Attribs->{'startup_hook'} = $startup_hook;
825 $confirmation = $readline->readline($prompt, $line);
829 local $SIG{'INT'} = \
&signal_int_handler
;
831 print STDERR
"$prompt$line";
832 # display an inverse-video space to indicate no-EOL.
833 print STDERR
"\x1B[7m \x1B[0m\n" unless $line =~ /\n$/;
834 $confirmation = <$terminal_fh>;
835 $confirmation = $line if $confirmation eq "\n";
843 # user pressed Ctrl-C
844 if($readline_support)
846 # show the original line
847 $readline->Attribs->{'line_buffer'} = $before;
848 $readline->Attribs->{'point'} = length $before;
849 $readline->redisplay;
851 $confirmation = undef;
853 # other exception happened
858 # restore low-level signal handler.
859 if($readline_support and %$old_sigaction)
861 sigaction SIGINT
, POSIX
::SigAction
->new($old_sigaction->{'HANDLER'});
864 # revert the line to the original as it was before,
865 # or apply what the user typed, depending on the confirmation result
866 if(defined $confirmation)
868 $line = $confirmation;
871 $processing_line->{'edited'} = 0;
875 if($OptVerbose and $processing_line->{'edited'})
877 print STDERR
"$lnum: " if $OptVerboseLnum;
878 # TODO: line numbering gets confusing when the newline is removed from or extra newlines added in $line
884 $output_cb->('append', $line, $cb_metadata) or return 0;
891 for my $file_idx (0..$#Files)
893 my $in_path = $Files[$file_idx];
894 my $out_path = $Output[$file_idx];
896 open my $in_fh, '<', $in_path or die "$in_path: $!\n";
898 my $output_handler = \
&buffered_overwrite
;
899 my $output_handler_meta = {};
901 if(defined $out_path)
903 open my $out_fh, '>', $out_path or die "$out_path: $!\n";
904 $output_handler = \
&filehandler_write
;
905 $output_handler_meta->{'fh'} = $out_fh;
906 $output_handler_meta->{'fname'} = $out_path;
910 open my $out_fh, '+<', $in_path or die "$in_path: $!\n";
911 $output_handler_meta->{'fh'} = $out_fh;
912 $output_handler_meta->{'fname'} = $in_path;
915 my $file_ok = process_file
($in_fh, \
@Commands, $output_handler, $output_handler_meta);
919 die "$in_path: $!\n";
922 close $in_fh or die "$in_path: $!\n";
923 close $output_handler_meta->{'fh'} or die "$output_handler_meta->{'fname'}: $!\n";
928 process_file
(\
*STDIN
, \
@Commands, \
&filehandler_write
, {'fh'=>\
*STDOUT
});
933 =head1 SIMILAR PROJECTS
937 =item L<https://github.com/andrewbihl/bsed>