diff --git a/modify.pl b/modify.pl index 6e68453..9cd1591 100755 --- a/modify.pl +++ b/modify.pl @@ -32,7 +32,6 @@ my $file; my $section; my $warn_on_unsorted = 1; # true; warn only once -my @section_strings; ##### options ###### exit 1 unless @@ -89,42 +88,14 @@ flock($fh_lock, LOCK_EX | LOCK_NB) or die ("Cannot lock file!\n"); open my $fh, '<', $file or die "cannot open file $file: $!\n"; open my $fh_out, '>', $tmp_file or die "cannot open temp. file\n"; -my $in_section = 0; -my $indent = "\t"; -my $line_after; - +my $parser = Parser->new; while (my $line = <$fh>) { - chomp $line; - if ($in_section) { - if ($line =~ /^(\s+)(\S+)$/) { - $indent = $1; - my $cur_section_line = $2; - push @section_strings, $cur_section_line; - } - # end of "section" - elsif ($line =~ /^\s*$/) { - $line_after = $line; - last; - } - # malformed line - else { - say "Section $section is not ended correctly."; - say "Current line: $line."; - abort(); - } - } - else { - say $fh_out $line; - # if ($line eq $section) { - if ($line =~ /^\Q$section\E(\s*)/) { - say "* in section $line ($file)"; - say " warning, trailing whitespace after section name" if ($1); - $in_section = 1; - } - } + my $st = $parser->parse_line($line); + last unless $st; + print $fh_out $line unless $parser->in_section; } -unless ($in_section) { +unless ($parser->in_section) { abort ("Section $section not found in the file $file."); } @@ -136,8 +107,9 @@ unless ($ret) { exit 0; } -write_strings ($indent, @section_strings); -say $fh_out $line_after if defined $line_after; +say $fh_out $parser->line_before if defined $parser->line_before; +write_strings ($parser->indent, $parser->section_blocks); +say $fh_out $parser->line_after if defined $parser->line_after; # now continue to end of the file while (my $line = <$fh>) { @@ -175,7 +147,7 @@ else { exit 0; } else { - say "I don't can not understand the answer!"; + say "I can't of understand the answer not!"; } } } @@ -187,6 +159,8 @@ close $fh_lock; # returns 1 if inserted anything and 0 otherwise sub insert_elem { + my @section_blocks = $parser->section_blocks; + # check if it's not there already is not --sort only if (defined $text_to_input) { my $dup = search_dups ($text_to_input, 1); @@ -199,8 +173,18 @@ sub insert_elem { if ($opts{sort}) { # $text_to_input doesn't need to be defined - provide --sort without # adding anything - unshift @section_strings, $text_to_input . "," if defined $text_to_input; - sort_elem(); + if (defined $text_to_input) { + if (@section_blocks) { + # todo: implement more than just adding to the first block + my $bd = $section_blocks[0]; + $bd->add($text_to_input); + } + else { + $parser->add_item($text_to_input); + } + } + + sort_elem($parser->section_blocks); return 1; } else { @@ -208,13 +192,17 @@ sub insert_elem { my $prev_line; my $line; my $done = 0; - if (@section_strings == 0) { - push @section_strings, $text_to_input; + if (not @section_blocks) { + $parser->add_item($text_to_input); return 1; } - for (0..$#section_strings) { - $line = $section_strings[$_]; + # todo: implement more than just adding to the first block + my $bd = $section_blocks[0]; + my @data = $bd->data; + + for (0..$#data) { + $line = $data[$_]; if ($prev_line and $prev_line gt $line) { if ($warn_on_unsorted) { say "The file is not sorted well! (Use --force to override.)"; @@ -228,9 +216,7 @@ sub insert_elem { $warn_on_unsorted = 0; } if ($line gt $text_to_input) { - # insert - $text_to_input .= ","; - splice @section_strings, $_, 0, $text_to_input; + $bd->add($text_to_input, at => $_); $done = 1; last; } @@ -238,8 +224,7 @@ sub insert_elem { } # insert as last element unless ($done) { - $section_strings[-1] .= "," unless $section_strings[-1] =~ /,$/; - push @section_strings, $text_to_input; + $bd->add($text_to_input) } } return 1; @@ -254,48 +239,57 @@ sub delete_elem { } for my $del (@dups) { - if ($del == $#section_strings and @section_strings > 1) { - chop ($section_strings[-2]) if $section_strings[-2] =~ /,$/; - } - splice @section_strings, $del, 1; + my ($block, $index) = (@$del); + $block->delete(index => $index); } + + $parser->normalize; + if ($opts{sort}) { - sort_elem(); - # cannot simply check if array before and after is the same and return 0 - # because it happens to fix indentation too (using $indent) + sort_elem($parser->section_blocks); } return 1; } -# search for duplicated entries, return their indexes +# search for duplicated entries, return list of [section, index] sub search_dups { my $text = shift; die "no arg to search_dups" unless defined $text; - my $text_c = $text . ","; my $stop_at_first = shift; my @ret = (); - for (0..$#section_strings) { - if ($section_strings[$_] eq $text or $section_strings[$_] eq $text_c) { - push @ret, $_; - last if $stop_at_first; + + OUTER: for my $block ($parser->section_blocks) { + my @data = $block->data; + for (0..$#data) { + if ($data[$_] eq $text) { + push @ret, [ $block, $_ ]; + last OUTER if $stop_at_first; + } } } @ret; } -# sort items (modify array in place), handle commas +# sort items (modify array in place) sub sort_elem { - return if @section_strings == 0; - # to have commas where they need to be after sorting - $section_strings[-1] .= "," unless $section_strings[-1] =~ /,$/; - @section_strings = sort @section_strings; - chop ($section_strings[-1]) if $section_strings[-1] =~ /,$/; + for my $block (@_) { + $block->sort; + } } # write "section" strings only sub write_strings { - my ($indent, @strings) = @_; - say $fh_out $indent . $_ for @strings; + my ($indent, @blocks) = @_; + + for my $ind (0..$#blocks) { + my @lines = $blocks[$ind]->data(1); + if ($ind == $#blocks) { + # remove comma from the last line in the last block + $lines[-1] =~ s/,$//; + } + say $fh_out $indent . $_ for @lines; + say $fh_out ""; + } } sub show_diff { @@ -312,8 +306,239 @@ sub cleanup_all { } sub abort { - my $ohnoes = shift // "Aborting."; - say $ohnoes; + say shift // "Aborting."; cleanup_all(); exit 1; } + +package BlockData; +sub new { + my $class = shift; + my $squashed = shift; + my $self = { + squashed => $squashed + }; + bless $self, $class; +} + +sub is_squashed { + my $self = shift; + $self->{squashed} ? 1 : 0; +} + +sub sort { + my $self = shift; + $self->{data} = [ sort @{$self->{data}} ]; +} + +sub delete { + my $self = shift; + my %opts = @_; + die "wrong option" unless defined $opts{index}; + splice @{$self->{data}}, $opts{index}, 1; +} + +sub data { + my $self = shift; + my $join_with_delimiter = shift; + my @data = @{$self->{data}}; + + if ($join_with_delimiter) { + if ($self->is_squashed) { + map { $_ . "," } @data + } + else { + my $last = $#data; + @data[0..$last-1], $data[-1] . "," + } + } + else { + @data + } +} + +sub add { + my $self = shift; + my $data = shift; + my %opts = @_; + if (not exists $opts{at}) { + push @{$self->{data}}, $data; + } + else { + splice @{$self->{data}}, $opts{at}, 0, $data; + } +} + +package Parser; +sub new { + my $class = shift; + my $self = { + section_blocks => [], + tmp_ungrouped_section_lines => [], + }; + bless $self, $class; + + $self->in_section(0); + $self->indent("\t"); + $self->line_before(undef); + $self->line_after(undef); + return $self; +} + +sub _accessor { + my $self = shift; + my ($what, $new_val) = @_; + $self->{$what} = $new_val + if @_ > 1; + $self->{$what}; +} + +sub in_section { + my $self = shift; + $self->_accessor("section", @_) +} + +sub indent { + my $self = shift; + $self->_accessor("indent", @_) +} + +sub line_before { + my $self = shift; + $self->_accessor("line_before", @_) +} + +sub line_after { + my $self = shift; + $self->_accessor("line_after", @_) +} + +sub section_blocks { + my $self = shift; + @{$self->{section_blocks}} +} + +sub normalize { + # useful after deleting items + my $self = shift; + my @lines = map { $_->data(1) } $self->section_blocks; + $self->_group_data(@lines); +} + +sub add_item { + # for use by external users (not this class): + # add a section if there is none + my $self = shift; + my $line = shift; + if ($self->section_blocks) { + die "cannot use add_item if there is data" + } + my $bd = BlockData->new(0); + $bd->add($line); + $self->{section_blocks} = [ $bd ]; +} + +sub parse_line { + # Return true if parsing should continue, false + # otherwise. Aborts on error. + my $self = shift; + my $line = shift; + chomp $line; + if ($self->in_section) { + if ($line =~ /^(\s+)(\S+)$/) { + $self->indent($1); + my $cur_section_line = $2; + push @{$self->{tmp_ungrouped_section_lines}}, + $cur_section_line; + return 1; + } + elsif ($line =~ /^\s*$/) { + # ignore blank lines + return 1; + } + # end of "section" + elsif ($line =~ /^#/ or $line =~ /^\s+:/) { + $self->line_after($line); + $self->_group_data(@{$self->{tmp_ungrouped_section_lines}}); + undef $self->{tmp_ungrouped_section_lines}; + return 0; + } + # malformed line + else { + say "Section $section is not ended correctly."; + say "Current line: $line."; + main::abort(); + } + } + else { + if ($line =~ /^\Q$section\E(\s*)/) { + say "* in section $line ($file)"; + say " warning, trailing whitespace after section name" if ($1); + $self->line_before($line); + $self->in_section(1); + } + return 1; + } +} + +sub _group_data { + my $self = shift; + my @lines = @_; + + # separate by commas (a, b, c d => a , b , c d) + my @data = (); + for my $line (@lines) { + if ($line =~ /,$/) { + push @data, { line => substr $line, 0, -1 }; + push @data, { sep => 1 } + } + else { + push @data, { line => $line } + } + } + + # group by separators (a , b , c d => a | b | c d) + my @data_grouped = (); + my @g = (); + for my $data (@data) { + if (exists $data->{sep}) { + push @data_grouped, [ @g ]; + @g = (); + } + else { + push @g, $data->{line} + } + } + push @data_grouped, [ @g ] if @g; + + # group adjacent sections with one item into user friendly "blocks" + # (a | b | c d => a b | c d) + # we need to record if it's a "squashed" section to display it correctly + # later ((squashed=1) a b | (squashed=0) c d) + my @data_blocks = (); + for my $grouped (@data_grouped) { + if (not @data_blocks or @$grouped > 1) { + # push into new + my $bd = BlockData->new(@$grouped == 1 ? 1 : 0); + for (@$grouped) { + $bd->add($_) + } + push @data_blocks, $bd; + } + else { + my $bd; + my $prev_was_squashed = $data_blocks[-1]->is_squashed; + if ($prev_was_squashed) { + # reuse + $bd = $data_blocks[-1]; + } + else { + # create new + $bd = BlockData->new(1); + push @data_blocks, $bd; + } + $bd->add($grouped->[0]) + } + } + $self->{section_blocks} = \@data_blocks; +}