]> git.donarmstrong.com Git - bin.git/blob - bibtex_to_paper
close FDs before execing PDF stuff
[bin.git] / bibtex_to_paper
1 #!/usr/bin/perl
2 # bibtex_to_paper opens the paper corresponding to a bibtex key
3 # and is released under the terms of the GNU GPL version 3, or any
4 # later version, at your option. See the file README and COPYING for
5 # more information.
6 # Copyright 2014 by Don Armstrong <don@donarmstrong.com>.
7
8
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 use File::Find;
16 use File::Basename qw(basename);
17 use File::Spec qw(rel2abs);
18 use Text::BibTeX;
19 use User;
20 use Data::Printer;
21 use POSIX;
22
23 use DBI;
24
25 =head1 NAME
26
27 bibtex_to_paper - opens the paper corresponding to a bibtex key
28
29 =head1 SYNOPSIS
30
31 bibtex_to_paper [options] bibtexkey
32
33  Options:
34   --bibtex, -b bibtex file to look up key in
35   --bibtex-cache, -c bibtex cache file
36   --build-cache, -B build cache using bibtex files
37   --pdf-dir pdf directory
38   --pdfviewer, -p pdf viewer to use
39   --debug, -d debugging level (Default 0)
40   --help, -h display this help
41   --man, -m display manual
42
43 =head1 OPTIONS
44
45 =over
46
47 =item B<--bibtex, -b>
48
49 Bibtex file to look key up in
50
51 =item B<--bibtex-cache, -c>
52
53 Bibtex cache file; rebuilt if bibtex file changes
54
55 =item B<--pdfviewer, -p>
56
57 PDF viewer to use; defaults to evince unless a .xoj exists, in which
58 case xournal is used.
59
60 =item B<--debug, -d>
61
62 Debug verbosity. (Default 0)
63
64 =item B<--help, -h>
65
66 Display brief usage information.
67
68 =item B<--man, -m>
69
70 Display this manual.
71
72 =back
73
74 =head1 EXAMPLES
75
76 bibtex_to_paper
77
78 =cut
79
80
81 use vars qw($DEBUG);
82
83 my %options = (debug           => 0,
84                help            => 0,
85                man             => 0,
86                'bibtex_cache'  => File::Spec->catfile(User->Home,'.bibtex_to_paper_cache'),
87               );
88
89 GetOptions(\%options,
90            'build_cache|build-cache!',
91            'bibtex|b=s@',
92            'bibtex_cache|bibtex-cache|c=s',
93            'pdfviewer|p=s',
94            'clear_cache|clear-cache!',
95            'papers_directory|papers-directory=s@',
96            'debug|d+','help|h|?','man|m');
97
98 pod2usage() if $options{help};
99 pod2usage({verbose=>2}) if $options{man};
100
101 $DEBUG = $options{debug};
102
103 my @USAGE_ERRORS;
104 if (not exists $options{bibtex} and
105     not exists $options{bibtex_cache}) {
106     push @USAGE_ERRORS,
107         "You must give at least one of --bibtex".
108         "or --bibtex-cache";
109 }
110
111 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
112
113 main();
114
115 sub main{
116
117     my $dbh;
118     my $sth;
119     if (exists $options{bibtex_cache}) {
120         my $initialize = 0;
121         if (-e $options{bibtex_cache}) {
122             ($dbh,$sth) = open_cache($options{bibtex_cache});
123         } else {
124             ($dbh,$sth) = initialize_database($options{bibtex_cache});
125         }
126     }
127
128     if (exists $options{clear_cache}) {
129         clear_cache($dbh,$sth);
130     }
131     my %entries;
132     if (exists $options{build_cache}) {
133         $options{bibtex} //= [];
134         $options{bibtex} =
135             [@ARGV,
136              @{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]},
137             ];
138         @ARGV = ();
139     }
140     if (exists $options{bibtex}) {
141         for my $bibtex_file (@{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]}) {
142             parse_bibtex_file($bibtex_file,\%entries);
143         }
144     }
145
146     if (exists $options{papers_directory} and
147         defined $dbh
148        ) {
149         load_papers_into_database($dbh,$sth,$options{papers_directory});
150     }
151
152     p %entries if $DEBUG;
153     if (keys %entries and
154         defined $dbh) {
155         load_bibtex_entries_into_database($dbh,$sth,\%entries);
156     }
157
158     p @ARGV if $DEBUG;
159     for my $bibtex_key (@ARGV) {
160         open_bibtex_key(\%options,$dbh,$sth,\%entries,$bibtex_key);
161     }
162
163 }
164
165 sub clear_cache {
166     my ($dbh,$sth) = @_;
167     $sth->{clear_papers_cache}->execute();
168     $sth->{clear_bibtex_cache}->execute();
169 }
170
171 sub load_papers_into_database {
172     my ($dbh,$sth,$dir) = @_;
173
174     my @dirs = ref($dir)?@{$dir}:$dir;
175
176     my $actually_load_it = sub {
177         if (/\.git/) {
178             $File::Find::prune = 1;
179             return;
180         }
181         return unless /\.pdf$/;
182         my $xoj = 0;
183         if (-e "${_}.xoj") {
184             $xoj = 1;
185         }
186         insert_or_replace_papers($dbh,$sth,basename($File::Find::name),File::Spec->rel2abs($_),$xoj);
187     };
188
189     my @pdfs;
190     find($actually_load_it,@dirs);
191 }
192
193 sub insert_or_replace_papers {
194     my ($dbh,$sth,$file_name,$file_loc,$has_xoj) = @_;
195     $sth->{insert_papers}->execute($file_name,$file_loc,$has_xoj);
196     $sth->{insert_papers}->finish();
197 }
198
199 sub load_bibtex_entries_into_database {
200     my ($dbh,$sth,$entries) = @_;
201     for my $entry (keys %{$entries}) {
202         next unless defined $entries->{$entry};
203         $sth->{insert_bibtex}->execute($entry,@{$entries->{$entry}}{qw(file_name doi html)});
204         $sth->{insert_bibtex}->finish();
205         print STDERR "inserted $entry {".join(',',map {defined $_?"'$_'":"'undef'"} %{$entries->{$entry}})."}\n" if $DEBUG;
206     }
207 }
208
209 sub open_bibtex_key {
210     my ($options,$dbh,$sth,$entries,$bibtex_key) = @_;
211     if (not defined $dbh) {
212         open_entry($dbh,$sth,$entries->{$bibtex_key},$options);
213     } else {
214         my $entry = select_entry_from_bibtex_key($dbh,$sth,$bibtex_key);
215         p $entry if $DEBUG;
216         open_entry($dbh,$sth,$entry,$options);
217     }
218 }
219
220 sub fork_exec {
221     my (@cmd) = @_;
222     my $child = fork();
223     if (not defined $child) {
224         die "Unable to fork for some reason: $!";
225     }
226     if ($child == 0) {
227         foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024))
228            { POSIX::close $_ }
229         open (STDIN, "</dev/null");
230         open (STDOUT, ">/dev/null");
231         open (STDERR, ">&STDOUT");
232         exec(@cmd);
233     } else {
234         return $child;
235     }
236
237 }
238
239 sub open_pdf {
240     my ($file_name,$options,$has_xoj) = @_;
241     print STDERR "opening $file_name\n" if $DEBUG;
242     if ($has_xoj) {
243         fork_exec('xournal',$file_name);
244     } else {
245         fork_exec('evince',$file_name)
246     }
247 }
248
249 sub open_browser{
250     my ($file) = @_;
251     fork_exec('sensible-browser',$file);
252 }
253
254 sub open_entry{
255     my ($dbh,$sth,$entry,$options) = @_;
256
257     return unless defined $entry and ref $entry and keys %{$entry};
258     if (defined $entry->{file_name} and length $entry->{file_name}) {
259         my $paper = select_one($dbh,$sth->{select_papers_by_name},$entry->{file_name});
260         if (not defined $paper) {
261             my ($pmid) = $entry->{file_name} =~ /pmid_(\d+)/;
262             if (defined $pmid and length $pmid) {
263                 $paper = select_one($dbh,$sth->{select_papers_by_pmid},'%pmid_'.$pmid.'.%');
264             }
265         }
266         p $paper if $DEBUG;
267         print STDERR $entry->{file_name} if $DEBUG;
268         if (defined $paper) {
269             open_pdf($paper->{path},$options,$paper->{has_xoj});
270             return;
271         }
272     }
273     if (defined $entry->{doi}) {
274         my $url = $entry->{doi};
275         $url =~ s{^doi://}{http://dx.doi.org/};
276         open_browser($url,$options);
277         return;
278     }
279     if (defined $entry->{html}) {
280         open_browser($entry->{html},$options);
281         return;
282     }
283 }
284
285 sub select_entry_from_bibtex_key{
286     my ($dbh,$sth,$bibtex_key) = @_;
287
288     my $entry = select_one($dbh,$sth->{select_bibtex_by_key},$bibtex_key);
289     if (not defined $entry) {
290         $bibtex_key =~ s/:.*$//;
291         $entry = select_one($dbh,$sth->{select_bibtex_by_approximate_key},$bibtex_key.'%');
292     }
293     return $entry;
294 }
295
296 sub select_one{
297     my ($dbh,$sth,@bind_vals) = @_;
298     $sth->execute(@bind_vals) or
299         die "Unable to select one: ".$dbh->errstr();
300     my $results = $sth->fetchall_arrayref({});
301     $sth->finish();
302     return ref($results)?$results->[0]:undef;
303 }
304
305 sub parse_bibtex_file {
306     my ($file,$entries) = @_;
307
308     my $bibfile = Text::BibTeX::File->new($file)
309         or die "Unable to open $file for reading: $!";
310     my @entry_comments;
311     my $entry;
312     while ($entry = Text::BibTeX::Entry->new($bibfile)) {
313         print STDERR "In Entry ".$entry->metatype() if $DEBUG;
314         if ($entry->metatype() == BTE_COMMENT) {
315             push @entry_comments,$entry->value();
316         } elsif ($entry->metatype() == BTE_REGULAR) {
317             my $entry_key = $entry->key();
318             if (not defined $entry_key) {
319                 @entry_comments = ();
320                 next;
321             }
322             my %entry_data;
323             # if there is a file comment, use it as the file name
324             for my $comment (@entry_comments) {
325                 next unless $comment =~ /^\s*file(?:name)?:?\s*(.+?)\s*$/i;
326                 next unless length $1;
327                 $entry_data{file_name} = $1.'.pdf';
328                 last;
329             }
330             my %field_prefix = (doi => 'doi://',
331                                 html => 'http://',
332                                 file => '',
333                                );
334             my %field_name = (doi => 'doi',
335                               html => 'html',
336                               file => 'file_name',);
337             for my $field (qw(file doi html)) {
338                 my $field_value = $entry->get($field);
339                 if (defined $field_value and $field_value =~ /\S+/) {
340                     $entry_data{$field_name{$field}} =
341                         $field_prefix{$field}.$field_value if
342                         not defined $entry_data{$field_name{$field}};
343                 }
344             }
345             $entries->{$entry_key} = {} if not defined $entries->{$entry_key};
346             for my $field (keys %entry_data) {
347                 $entries->{$entry_key}{$field} = $entry_data{$field} if
348                     defined $entry_data{$field};
349             }
350             # reset the entry comments
351             @entry_comments = ();
352         } else {
353             # do nothing
354         }
355         print STDERR "\n" if $DEBUG;
356     }
357     return $entries;
358 }
359
360
361 sub initialize_database {
362     my ($cache) = @_;
363     return open_cache($cache,1);
364 }
365
366 sub open_cache {
367     my ($cache,$initialize) = @_;
368     my $dbh = DBI->connect("dbi:SQLite:dbname=$cache","","") or
369         die "Unable to open/create database $cache";
370     if ($initialize) {
371         $dbh->do("DROP TABLE IF EXISTS bibtex;");
372         $dbh->do("DROP TABLE IF EXISTS papers;");
373         $dbh->do(<<EOF);
374 CREATE TABLE bibtex (
375 bibtex_key TEXT PRIMARY KEY,
376 file_name TEXT,
377 doi TEXT,
378 html TEXT
379 );
380 EOF
381         $dbh->do(<<EOF);
382 CREATE UNIQUE INDEX bibtex_file_name ON bibtex(file_name);
383 EOF
384         $dbh->do(<<EOF);
385 CREATE UNIQUE INDEX bibtex_bibtex_key ON bibtex(bibtex_key);
386 EOF
387         $dbh->do(<<EOF);
388 CREATE TABLE papers (
389 file_name TEXT PRIMARY KEY,
390 path TEXT,
391 has_xoj BOOLEAN
392 );
393 EOF
394         $dbh->do(<<EOF);
395 CREATE UNIQUE INDEX papers_path ON papers(path);
396 EOF
397         $dbh->do(<<EOF);
398 CREATE UNIQUE INDEX papers_file_name ON papers(file_name);
399 EOF
400     }
401     my %s =
402         (insert_papers => <<'EOF',
403 INSERT OR REPLACE INTO papers(file_name,path,has_xoj) VALUES (?,?,?);
404 EOF
405          insert_bibtex => <<'EOF',
406 INSERT OR REPLACE INTO bibtex (bibtex_key,file_name,doi,html) VALUES (?,?,?,?);
407 EOF
408          select_papers_by_name => <<'EOF',
409 SELECT * FROM papers WHERE file_name = ?;
410 EOF
411          select_papers_by_pmid => <<'EOF',
412 SELECT * FROM papers WHERE file_name LIKE ?;
413 EOF
414          select_papers_by_path => <<'EOF',
415 SELECT * FROM papers WHERE path = ?;
416 EOF
417          select_bibtex_by_key => <<'EOF',
418 SELECT * FROM bibtex WHERE bibtex_key = ?;
419 EOF
420          select_bibtex_by_approximate_key => <<'EOF',
421 SELECT * FROM bibtex WHERE bibtex_key LIKE ?;
422 EOF
423          select_bibtex_by_file_name => <<'EOF',
424 SELECT * FROM bibtex WHERE file_name = ?;
425 EOF
426          clear_papers_cache => <<'EOF',
427 DELETE FROM papers;
428 EOF
429          clear_bibtex_cache => <<'EOF',
430 DELETE FROM bibtex;
431 EOF
432         );
433     my $st;
434     for my $key (keys %s) {
435         $st->{$key}=$dbh->prepare($s{$key}) //
436             die "Unable to prepare sql statement: ".$dbh->errstr;
437     }
438     return ($dbh,$st);
439 }
440
441
442 __END__