build: add error if `chroot --status` fails (#1151)
[aurutils.git] / lib / aur-search
blob8e5693e19867bbeda8df6aed57eac554069df956
1 #!/usr/bin/env perl
2 use strict;
3 use warnings;
4 use v5.20;
6 use POSIX qw(strftime setlocale LC_NUMERIC LC_TIME LC_ALL);
7 use locale qw(:numeric :time);
9 use Term::ANSIColor qw(:constants);
10 use constant OSC8 => "\033]8";
11 use constant ST => "\033\\";
13 use AUR::Json qw(parse_json_aur write_json);
14 use AUR::Query qw(query query_multi);
15 use AUR::Options qw(add_from_stdin);
16 my $argv0 = 'search';
17 my $aur_location = $ENV{AUR_LOCATION} // 'https://aur.archlinux.org';
19 # sprintf, strftime()
20 setlocale(LC_NUMERIC, 'C');
21 setlocale(LC_TIME, 'C');
23 sub format_long {
24 my $pkg = shift;
25 # Custom fixed order for info output
26 my @keys = (
27 'Name' , 'PackageBase' , 'Version' , 'Description' , 'URL',
28 'Keywords' , 'License' , 'Maintainer' , 'Submitter' , 'NumVotes',
29 'Popularity' , 'OutOfDate' , 'FirstSubmitted' , 'LastModified', 'Depends',
30 'MakeDepends', 'CheckDepends', 'OptDepends'
32 my $url = join("/", $aur_location, "packages", $pkg->{'Name'});
33 say BOLD, sprintf("%-15s", "AUR URL:"), RESET, ' ', $url;
35 # XXX: overlap with info_expand_field() from aur-format
36 for my $k (@keys) {
37 my $f;
39 if (ref $pkg->{$k} eq 'ARRAY') {
40 $f = join(' ', @{$pkg->{$k} // ['-']});
41 } elsif ($k eq 'LastModified' or $k eq 'OutOfDate' or $k eq 'FirstSubmitted') {
42 $f = defined $pkg->{$k} ? gmtime $pkg->{$k} : '-';
43 } else {
44 $f = $pkg->{$k} // '-';
46 say BOLD, sprintf("%-15s", "$k:"), RESET, ' ', $f;
50 sub format_short {
51 my $pkg = shift;
52 my $name = $pkg->{'Name'};
53 my $desc = $pkg->{'Description'};
54 my $ver = $pkg->{'Version'};
55 my $votes = $pkg->{'NumVotes'};
57 # Formatted fields
58 my $ood = defined $pkg->{'OutOfDate' }
59 ? strftime("(Out-of-date: %d %B %Y)", gmtime $pkg->{'OutOfDate'}) : "";
60 my $orph = defined $pkg->{'Maintainer'}
61 ? "" : "(Orphaned) ";
62 my $pop = sprintf("%.2f", $pkg->{'Popularity'});
63 my $pre = sprintf("%s%saur/%s%s%s", BOLD, BLUE, RESET, BOLD, $name);
65 # OSC sequences
66 if (not exists $ENV{ANSI_COLORS_DISABLED}) {
67 $pre = sprintf("%s;;%s%s%s%s;;%s", OSC8, "$aur_location/packages/$name", ST, $pre, OSC8, ST);
70 say $pre, ' ', GREEN, $ver, RESET, ' (+', $votes, ' ', $pop, '%) ',
71 $orph, BOLD, RED, $ood, RESET;
72 say ' ', $desc // '-';
75 # Set union by hash value
76 sub results_union {
77 my ($target, $results, $seen, $union_key) = @_;
79 if (!keys %{$seen}) {
80 %{$seen} = map { $_->{$union_key} => 1 } @{$results};
82 push(@{$results}, grep { !$seen->{$_->{$union_key}}++ } @{$target});
85 # Set intersection by hash value
86 sub results_isect {
87 my ($target, $results, $isect_key) = @_;
88 my %seen = map { $_->{$isect_key} => 1 } @{$target};
90 @{$results} = grep { $seen{$_->{$isect_key}} } @{$results};
93 # Sort a flattened array of hashes
94 sub results_rsort {
95 my ($results, $sort_key, $reverse) = @_;
97 # Sort entries by value of specified key
98 if ($sort_key eq 'Popularity' or $sort_key eq 'NumVotes') {
99 @{$results} = sort { $a->{$sort_key} <=> $b->{$sort_key} } @{$results};
100 } elsif (length $sort_key) {
101 @{$results} = sort { $a->{$sort_key} cmp $b->{$sort_key} } @{$results};
103 if ($reverse) {
104 @{$results} = reverse @{$results};
108 unless(caller) {
109 # option handling
110 use Getopt::Long;
111 my $opt_multiple = 'section';
112 my $opt_type = 'search';
113 my $opt_search_by = 'name-desc';
114 my $opt_sort_key = '';
115 my $opt_color = 'auto';
116 my $opt_reverse = 0;
117 my $opt_format = '';
119 # XXX: add option to disable set operations, --time-format
120 GetOptions (
121 'a|any' => sub { $opt_multiple = 'union' },
122 'i|info' => sub { $opt_type = 'info' },
123 's|search' => sub { $opt_type = 'search' },
124 'd|desc' => sub { $opt_search_by = 'name-desc' },
125 'm|maintainer' => sub { $opt_search_by = 'maintainer' },
126 'n|name' => sub { $opt_search_by = 'name' },
127 'depends' => sub { $opt_search_by = 'depends' },
128 'makedepends' => sub { $opt_search_by = 'makedepends' },
129 'optdepends' => sub { $opt_search_by = 'optdepends' },
130 'checkdepends' => sub { $opt_search_by = 'checkdepends' },
131 'submitter' => sub { $opt_search_by = 'submitter' },
132 'provides' => sub { $opt_search_by = 'provides' },
133 'conflicts' => sub { $opt_search_by = 'conflicts' },
134 'replaces' => sub { $opt_search_by = 'replaces' },
135 'keywords' => sub { $opt_search_by = 'keywords' },
136 'groups' => sub { $opt_search_by = 'groups' },
137 'comaintainers' => sub { $opt_search_by = 'comaintainers' },
138 'q|short' => sub { $opt_format = 'short' },
139 'v|verbose' => sub { $opt_format = 'long' },
140 'J|json|raw' => sub { $opt_format = 'json' },
141 'color=s' => \$opt_color,
142 'r|reverse' => \$opt_reverse,
143 'k|key=s' => \$opt_sort_key
144 ) or exit(1);
146 # Handle '-' to take packages from stdin
147 add_from_stdin(\@ARGV, ['-', '/dev/stdin']);
149 if (not scalar @ARGV) {
150 say STDERR "$argv0: at least one search term needed";
151 exit(1);
154 # Colored messages on both stdout and stderr may be desired if stdout is not
155 # connected to a terminal, e.g. when piping to less -R. (#585) When printing
156 # to a file, they should be disabled instead. Default to `--color=auto` but
157 # allow specifying other modes.
158 my $colorize = 0;
159 if (not defined $ENV{AUR_DEBUG}) {
160 if (($opt_color eq 'auto' and -t STDOUT) or $opt_color eq 'always') {
161 $colorize = 1;
164 if ($colorize == 0) {
165 $ENV{ANSI_COLORS_DISABLED} = 1;
168 # Set format depending on query type (#319)
169 if (not length($opt_format)) {
170 $opt_format = $opt_type eq 'info' ? 'long' : 'short';
173 # Retrieve JSON responses
174 my @results;
176 # TODO: handle '-' as stdin argument
177 if ($opt_type eq 'search') {
178 # Apply union/intersection starting at the first argument
179 my $first = shift;
180 my %query_args = (type => 'search', by => $opt_search_by, callback => \&parse_json_aur);
181 @results = query(term => $first, %query_args);
183 my %seen;
184 for my $arg (@ARGV) {
185 my @next = query(term => $arg, %query_args);
187 if ($opt_multiple eq 'union') {
188 results_union(\@next, \@results, \%seen, 'Name');
190 elsif ($opt_multiple eq 'section') {
191 results_isect(\@next, \@results, 'Name');
193 else {
194 push(@results, @next);
198 elsif ($opt_type eq 'info') {
199 # Union/intersection do not apply to info-style requests
200 @results = query_multi(terms => \@ARGV, type => 'info', callback => \&parse_json_aur);
202 exit(1) if scalar @results == 0;
204 # Apply sorting criteria
205 if (length $opt_sort_key or $opt_reverse) {
206 results_rsort(\@results, $opt_sort_key, $opt_reverse);
208 # Format results to standard output
209 if ($opt_format eq 'short') {
210 map { format_short($_) } @results;
212 elsif ($opt_format eq 'long') {
213 my $i = 0;
214 map { format_long($_); say '' if ++$i < scalar @results } @results;
216 elsif ($opt_format eq 'json') {
217 say write_json(\@results);
219 else {
220 die 'invalid format';
224 # vim: set et sw=4 sts=4 ft=perl: