#! /usr/bin/perl # The copyright notice and license are in the POD at the bottom. use strict; use warnings; use Locale::PO qw(); use Getopt::Long qw(GetOptions :config bundling); use autouse 'Pod::Usage' => qw(pod2usage); use autouse 'File::Spec::Functions' => qw(catfile); my $VERSION = "1.2"; sub show_version { print "msgaccel-prepare $VERSION\n"; pod2usage({-verbose => 99, -sections => "COPYRIGHT AND LICENSE", -exitval => 0}); } { package Contextline; use fields qw(lineno contexts); sub new { my($self, $lineno, $contexts) = @_; $self = fields::new($self) unless ref $self; $self->{lineno} = $lineno; $self->{contexts} = $contexts; return $self; } } my @Srcpath; my $Accelerator_tag; # Each key is a file name. # Each value is a reference to an array of references to Contextline # pseudo-hashes. The array is in ascending order by {lineno}. my %Srcfiles; sub open_file_on_path ($@) { my($fname, @path) = @_; if (@path) { my @warnings; foreach my $dir (@path) { my $full_fname = catfile($dir, $fname); if (open my $fh, "<", $full_fname) { return($fh, $full_fname); } else { push @warnings, "$full_fname: $!\n"; } } # Didn't find $name anywhere on @path. warn $_ foreach @warnings; return; } else { # no path if (open my $fh, "<", $fname) { return($fh, $fname); } else { warn "$fname: $!\n"; return; } } # not reached } # Scan the file $src_fname for gettext_accelerator_context directives, # cache the result in %Srcfiles, and return it in that format. # Cache and return [] if the file cannot be read on @Srcpath. sub contextlines ($) { my($src_fname) = @_; return $Srcfiles{$src_fname} if exists($Srcfiles{$src_fname}); my @contextlines = (); if (my($src_fh, $src_full_fname) = open_file_on_path($src_fname, @Srcpath)) { my @prevctxs; local $_; while (<$src_fh>) { chomp; if (/^\}/ && @prevctxs) { push @contextlines, Contextline->new($., [@prevctxs = ()]); } if (my($contexts) = /\[gettext_accelerator_context\(([^()]*)\)\]/) { my @contexts = grep { $_ ne "" } split(/\s*,\s*/, $contexts); s/^\./${src_fname}:/ foreach @contexts; warn "$src_full_fname:$.: Previous context not closed\n" if @prevctxs && @contexts; warn "$src_full_fname:$.: Context already closed\n" if !@prevctxs && !@contexts; push @contextlines, Contextline->new($., [@prevctxs = @contexts]); } elsif (/gettext_accelerator_context/) { warn "$src_full_fname:$.: Suspicious non-directive: $_\n"; } } warn "$src_full_fname:$.: Last context not closed\n" if @prevctxs; } # if opened ok return $Srcfiles{$src_fname} = \@contextlines; } sub contexts ($$) { my($srcfile, $lineno) = @_; # Could use a binary search here. my $contextlines = contextlines($srcfile); my @contexts = (); foreach my Contextline $contextline (@{$contextlines}) { return @contexts if $contextline->{lineno} > $lineno; @contexts = @{$contextline->{contexts}}; } return (); } sub gather_accelerator_contexts ($$) { my($pos, $po_fname) = @_; foreach my $po (@$pos) { my $automatic = $po->automatic(); $automatic =~ s/^\[gettext_accelerator_context\(.*(?:\n|\z)//mg if defined($automatic); if ($po->msgid() =~ /\Q$Accelerator_tag/s) { my @po_contexts = (); foreach my $ref (split(' ', $po->reference())) { my @parts = split(/\:/, $ref); warn "weird reference: $ref\n", next unless @parts == 2; my @ref_contexts = contexts($parts[0], $parts[1]); if (@ref_contexts) { push @po_contexts, grep { $_ ne "IGNORE" } @ref_contexts; } else { warn "$ref: No accelerator context for msgid " . $po->msgid() . "\n"; } } if (@po_contexts) { # sort and uniquify @po_contexts = sort keys %{{map { $_ => 1 } @po_contexts}}; $automatic .= "\n" if defined($automatic) and $automatic ne ""; $automatic .= "accelerator_context(" . join(", ", @po_contexts) . ")"; } } $po->automatic($automatic); } } GetOptions("srcdir|source-directory|S=s" => \@Srcpath, "accelerator-tag=s" => sub { my($option, $value) = @_; die "Cannot use multiple --accelerator-tag options\n" if defined($Accelerator_tag); die "--accelerator-tag requires a single-character argument\n" if length($value) != 1; $Accelerator_tag = $value; }, "help" => sub { pod2usage({-verbose => 1, -exitval => 0}) }, "version" => \&show_version) or exit 2; $Accelerator_tag = "~" unless defined $Accelerator_tag; print(STDERR "$0: missing file operand\n"), exit 2 unless @ARGV; print(STDERR "$0: too many operands\n"), exit 2 if @ARGV > 1; my($po_fname) = @ARGV; my $pos = Locale::PO->load_file_asarray($po_fname) or die "$po_fname: $!"; gather_accelerator_contexts($pos, $po_fname); Locale::PO->save_file_fromarray($po_fname, $pos) or die "$po_fname: $!"; __END__ =head1 NAME msgaccel-prepare - Augment a PO file with information for detecting accelerator conflicts. =head1 SYNOPSIS B [I