spgsearch - простой поиск файлов

Подошли ко мне товарищи и посетовали, мол в Xfce (Thunar) нету поиска по файловой системе штатного. А у них там шара огромная, через gvfs/fuse/samba зацеплена. И надо искать файлы, директории, и директории, где лежат файлы. Документооборот у них так выстроен. А я что? А я, по какой-то причине, за многое лето не сталкивался с необходимостью что-то искать через графический интерфейс. И стало мне интересно…

Шустрый поиск по интернетам привёл меня к catfish. Но это всё оказалось слабо пригодной поделкой. Шустро, аккуратно, но… бесполезно. Мне вот документ надо искать по фрагменту имени. И чтобы потом Libreoffice нормально его открыл. Или чтобы можно было открыть сразу директорию, файл искомый содержащую. Так как нужен мне был не совсем он, а соседний файл, но часть имени я помнил только вот от этого файла. И мне совсем не нужны превьюшки и аккуратные менюшки. Мне можно кондово, не притязателен…

И решил я написать свой собственный велосипед. Конечно же на perl, с интерфейсом на GTK+ 3, и обязательно с поддержкой потоков (perl threads), чтобы, пока там активно идёт поиск, можно было изучать предварительные результаты. Прервав, например, процесс, если уже нашлось то, что искали. Или если передумал.

Правда есть нюанс: нужен модифицированный вариант File::Find::utf8. В том же Debian в репозиториях его нет. Но он, конечно же, есть на CPAN. А у нас есть dh-make-perl, который может нам быстро собрать нужный .deb пакет из любого актуального модуля с CPAN. А ещё версия perl’а нужна с поддержкой тех самых perl threads (5.008+).

Код, собственно:

#!/usr/bin/perl
##############################################################################
# Simple Perl+Gtk3 SEARCH (spgsearch).
# Простая утилита для поиска файлов/директорий.
# Написана на Perl и GTK+ 3, с применением потоков (perl threads)
# и полной поддержкой юникода (UTF-8).
# Предназначена для восполнения отсутствия "родного" поиска в Thunar/PCmanFM.
# Что, впрочем, не мешает вам применять её ещё как-то.
##############################################################################
use strict;
use warnings;
use utf8::all;			# :all - дескрипторы ввода/вывода и @ARGV сразу в utf8
use Glib qw(TRUE FALSE);
use Gtk3 -init;	
use Gtk3::SimpleList;
use File::Find::utf8;		# искать на CPAN, оригинальный File::Find не умеет utf8
use POSIX qw(strftime);
use Config;			# представляет нам параметры сборки интерпретатора
use threads;
use threads::shared;

# Для корректной работы нам необходима версия интерпретатора не ниже 5.008,
# интерпретатор должен быть скомпилирован с тредами.
die("Для работы нужна версия perl не ниже 5.008!\n") if($] lt '5.008');
die("Для работы необходима поддержка тредов!\n") unless($Config{useithreads});
# Запускаться без путей для поиска мы тоже не должны.
die("Необходимо указать не менее одной директории для поиска!\n") if($#ARGV < 0);

# Окно ввода информации от пользователя. Функция должна вернуть
# хеш с шаблоном и остальными параметрами поиска.
my $search = process_input_window();
exit(0) unless(defined($search->{'pattern'}));	# пользователь закрыл окно

# Для поиска "в фоне" (чтобы GTK не залипал), нам нужны треды. И общие
# структуры данных тоже нужны. Оглашаем структуры, запускаем тред с поиском.
my $cur_dir_shared :shared = "";
my @founded_type_shared :shared = ();
my @founded_filename_shared :shared = ();
my @founded_dirpath_shared :shared = ();
my @founded_filepath_shared :shared = ();
my @founded_created_shared :shared = ();
my @founded_modified_shared :shared = ();
my $thread = threads->create(\&thread_code);	# треду можно и параметры передать, если что

# Останавливаем тред "руками", если при закрытии пользователем
# окна с результатами поиска, он ещё запущен.
$thread->kill('SIGTERM') if(process_result_window());
$thread->join();	# нужно, чтобы интерпретатор произвёл вычистку ресурсов

exit(0);


# Функция, запрашивающая у пользователя информацию для поиска.
# Просто работа с одним окном и вводом.
sub process_input_window {
	# Сразу создаём хеш для результата.
	my %result = (
		'pattern'	=> undef,
		'file'		=> 0,
		'directory'	=> 0
	);

	# Создаём виджеты.
	my $window = Gtk3::Window->new();
	my $vbox = Gtk3::VBox->new(0, 0);
	my $hbox1 = Gtk3::HBox->new(0, 0);
	my $hbox2 = Gtk3::HBox->new(0, 0);
	my $hbox3 = Gtk3::HBox->new(0, 0);
	my $label1 = Gtk3::Label->new('Введите фрагмент искомых названий:');
	my $field = Gtk3::Entry->new();
	my $button = Gtk3::Button->new('Поиск');
	my $label2 = Gtk3::Label->new('Тип:');
	my $file_chk = Gtk3::CheckButton->new_with_label('Файлы');
	my $dir_chk = Gtk3::CheckButton->new_with_label('Директории');

	# Ну и надо их правильно разместить.
	$window->add($vbox);
	$vbox->pack_start($hbox1, 0, 0, 3);
	$vbox->pack_start($hbox2, 0, 0, 0);
	$vbox->pack_start($hbox3, 0, 0, 3);
	$hbox1->pack_start($label1, 0, 0, 3);
	$hbox2->pack_start($field, 1, 1, 1);
	$hbox2->pack_end($button, 0, 0, 1);
	$hbox3->pack_start($label2, 0, 0, 3);
	$hbox3->pack_start($file_chk, 0, 0, 3);
	$hbox3->pack_start($dir_chk, 0, 0, 3);	

	# Указываем параметры виджетов,
	# те, которые нельзя было указать при создании виджетов.
	$window->set_resizable(0);
	$window->set_title('Поиск файлов и директорий');
	$window->signal_connect(delete_event => sub {		# функция, вызываемая при нажатии на закрытие окна
		Gtk3->main_quit();				# завершение рабочего цикла Gtk
	});
	$button->signal_connect(clicked => sub {		# функция, вызываемая при нажатии на кнопку "Поиск"
		# Мы будем делать что-либо, только если пользователь
		# ввёл хоть что-то в поле. Иначе просто возвращаем "ничего".
		my $value = $field->get_text();
		return() unless(length($value));
		# Теперь забираем ввод пользователя и завершаем работу Gtk.
		$result{'pattern'} = $value;
		$result{'file'} = $file_chk->get_active();
		$result{'directory'} = $dir_chk->get_active();
		Gtk3->main_quit();				# схлопнули цикл Gtk
	});
	$file_chk->set_active(1);				# чекбоксы надо "отметить"
	$dir_chk->set_active(1);

	$window->show_all();					# окно отмечено для отрисовки со всем своим содержимым
	Gtk3->main();						# рабочий цикл Gtk запущен

	# Цикл завершён. И иначе мы тут не были бы.
	# Самое время "скрывать" родительское окно и поочерёдно
	# удалить все виджеты. Начиная с окна.
	$window->hide();

	$window->destroy();
	$vbox->destroy();
	$hbox1->destroy();
	$hbox2->destroy();
	$hbox2->destroy();
	$label1->destroy();
	$field->destroy();
	$button->destroy();
	$label2->destroy();
	$file_chk->destroy();
	$dir_chk->destroy();

	return(\%result);					# тот самый хеш
}


# Функция работы с окном вывода результатов поиска.
# Тут мы не только отрисовываем окном, но и получаем информацию
# от треда, в котором проводится поиск.
sub process_result_window {
	my @files = ();						# массив для хранения информации, получаемой и поискового треда
	my $still_run = 1;					# флаг завершения поиска

	# Создаём окно и остальные виджеты. Потом заполоняем виджетами окно.
	# После заполнения окна, устанавливаем виджетам "дополнительные"
	# свойства, типа подключения обработчиков "сигналов" (например нажатие на кнопку).
	my $window = Gtk3::Window->new('toplevel');
	my $vbox = Gtk3::VBox->new(0, 0);
	my $hbox1 = Gtk3::HBox->new(0, 0);
	my $hbox2 = Gtk3::HBox->new(0, 0);
	my $hbox3 = Gtk3::HBox->new(0, 0);
	my $openobj = Gtk3::Button->new('Открыть сам объект');
	my $opendir = Gtk3::Button->new('Открыть директорию');
	my $scroll = Gtk3::ScrolledWindow->new();
	my $slist = Gtk3::SimpleList->new (
		'Тип'		=> 'text',
		'Файл'		=> 'text',
		'Создан'	=> 'text',
		'Изменён'	=> 'text'
	);
	my $status = Gtk3::Statusbar->new();

	$window->add($vbox);
	$vbox->pack_start($hbox1, 0, 0, 1);
	$vbox->pack_start($hbox2, 1, 1, 0);
	$vbox->pack_start($hbox3, 0, 0, 0);
	$hbox1->pack_start($openobj, 0, 0, 1);
	$hbox1->pack_start($opendir, 0, 0, 1);
	$hbox2->pack_start($scroll, 1, 1, 0);
	$hbox3->pack_start($status, 1, 1, 0);
	$scroll->add($slist);

	$window->set_title('Результаты поиска');
	$window->signal_connect(delete_event => sub {		# событие закрытия окна
		Gtk3->main_quit();
	});
	$openobj->signal_connect(clicked => sub {		# событие нажатия на кнопку открытия файла
		my ($selected) = $slist->get_selected_indices();
		return() unless(defined($selected));
		system("exo-open", "--launch", "FileManager", $files[$selected]->[3]);
	});
	$opendir->signal_connect(clicked => sub {		# событие нажатия на кнопку открытия директории
		my ($selected) = $slist->get_selected_indices();
		return() unless(defined($selected));
		system("exo-open", "--launch", "FileManager", $files[$selected]->[2]);
	});
	$scroll->set_policy(qw/automatic automatic/);
	my $status_context = $status->get_context_id('search_progress');	# получаем номер для индикатора статуса
	$status->push($status_context, "Идёт поиск");		# начальная запись строки состояния

	# Создаём объект таймера. Он будет отрабатывать, пока не вернёт FALSE.
	my $timerobj = Glib::Timeout->add(250, sub {
		return(TRUE) if(list_update(\@files, $status, $slist, $status_context));
		# Завершаем работу таймера. Флаг надо не забыть поставить в undef.
		$still_run = undef;
		return(FALSE);
	});

	# Отмечаем окно и виджеты для отображения.
	# Запускаем цикл.
	$window->show_all();
	Gtk3->main();

	# Цикл Gtk завершён. "Гасим" окно.
	# Удаляем все виджеты.
	$window->hide();

	$window->destroy();
	$vbox->destroy();
	$hbox1->destroy();
	$hbox2->destroy();
	$hbox3->destroy();
	$openobj->destroy();
	$opendir->destroy();
	$scroll->destroy();
	$slist->destroy();
	$status->destroy();

	return($still_run);					# флаг надо вернуть
}


# Функция конвертирования целочисленного значения unix timestamp
# в нечто более читаемое для человека, например 26.08.18 22:44.
sub timestamp_to_string {
	my $stamp = shift();
	my $string = strftime('%d.%m.%Y %H:%M', localtime($stamp));
	return($string);
}


# Функция, вызываемая из таймера Glib. Вынесена сюда.
sub list_update {
	my $files = shift();
	my $status = shift();
	my $slist = shift();
	my $status_context = shift();

	# Флаг! Флаг == 1, пока $cur_dir_shared != undef.
	# Пока он != undef, надо обновлять запись status,
	# то есть стереть и вывести туда текущую запись
	# из $cur_dir_shared.
	my $still_run = 1;

	$status->remove_all($status_context);
	# Обязательно! Блокируем переменную.
	# Блокировка снимется сама, прямо по завершению
	# исполнения на данном уровне. То есть при return().
	lock($cur_dir_shared);
	if(defined($cur_dir_shared)) {
		$status->push($status_context, "Просмотр " . $cur_dir_shared);
	}
	else {			# Поиск завершён! Флаг в undef, запись в status.
		$status->push($status_context, "Поиск завершён");
		$still_run = undef;
	}

	# Таки блокирвем остальные ресуры. Они освободятся по return().
	lock(@founded_type_shared);
	lock(@founded_filename_shared);
	lock(@founded_dirpath_shared);
	lock(@founded_filepath_shared);
	lock(@founded_created_shared);
	lock(@founded_modified_shared);

	# Итак, у нас тут несколько массивов с одинаковой глубиной.
	# Их надо пересобрать как записи для массива @{$files}.
	# Берём индекс первого массива, и, считая его глубиной всех массивов, крутим цикл,
	# делаем shift над массивами, push'им полученные записи как @{$files}->[записи].
	# Заодно push'им часть полей как @{$slist->{data}}->[поля].
	my $founded_last_index = $#founded_type_shared;		# индекс @founded_type_shared берём как общую глубину
	for(my $i = 0; $i <= $founded_last_index; $i++) {
		my @add_this = (
			shift(@founded_type_shared),		# берём крайнюю запись о типе
			shift(@founded_filename_shared),	# крайнюю запись с именем
			shift(@founded_dirpath_shared),		# крайнюю запись с директорией
			shift(@founded_filepath_shared),	# крайнюю запись с полным путём
			shift(@founded_created_shared),		# крайнюю запись с временем создания
			shift(@founded_modified_shared)		# ну и временем изменения
		);

		push(@{$files}, \@add_this);			# запушили в основной массив

		# И добавляем в интерфейс
		push(@{$slist->{data}}, [
			type_id_to_text($add_this[0]),		# сделав читаемым тип
			$add_this[3],
			timestamp_to_string($add_this[4]),	# сделав читаемым unix timestamp
			timestamp_to_string($add_this[5])	# и ещё один timestamp
		]);
	}

	return($still_run);					# Флаг! Иначе как понять, запущен ли тред?
}


# Код треда для поиска. Для работы требуются массив @ARGV
# и хешреф %{$search}. Они объявлены в "зоне видимости" основного кода,
# и доступны вот прямо из функции (благо тред получит копию структур данных).
# В противном случае мы их передали бы через @_.
# Логика построена вокруг File::Find, то есть мы работаем в цикле, сверяя параметры,
# отдаваемые внутрь блока кода. Самые ходовые:
# $_ - имя объекта,
# $File::Find::name - полный путь к объекту,
# $File::Find::dir - полный путь к директории, где нашёлся объект.
sub thread_code {
	find(sub {
		# Копируем переменные File::Find.
		my $filename = $_;
		my $filepath = $File::Find::name;
		my $dirpath = $File::Find::dir;
		my $type = undef;	# тип пока не известен

		# Ставим блокировку на переменную с текущем путём и переписываем её.
		lock($cur_dir_shared);
		$cur_dir_shared = $dirpath;

		# Мы нашли файл
		if(-f $filepath) {
			return() unless($search->{'file'});		# но не искали файлы
			$type = 0;
		}
		# Мы нашли директорию
		elsif(-d $filepath) {
			return() unless($search->{'directory'});	# но не искали директории
			$type = 1;
		}
		return() unless(defined($type));			# тип всё ещё не известен? следующий!
		return() unless($filename =~ /$search->{'pattern'}/i);

		# Берём информацию об объекте. Если не получилось,
		# то переходим к следующему.
		my @stat = stat($filepath);
		return() if($#stat < 0);

		# $filepath содержит нечто, похожее на gvfs. Заменим на соответствующий uri?
		if($filepath =~ /^\/run\/user\/\d+\/gvfs\/smb-share:server=([^,]+),share=([^\/]+)\/(.*)$/) {
			$filepath = "smb://$1/$2/$3";
		}
		# Аналогично и для $dirpath...
		if($dirpath =~ /^\/run\/user\/\d+\/gvfs\/smb-share:server=([^,]+),share=([^\/]+)\/(.*)$/) {
			$dirpath = "smb://$1/$2/$3";
		}

		# Ставим блокировки ресурсов и пушим их в соответствующие массивы.
		lock(@founded_type_shared);
		lock(@founded_filename_shared);
		lock(@founded_dirpath_shared);
		lock(@founded_filepath_shared);
		lock(@founded_created_shared);
		lock(@founded_modified_shared);

		push(@founded_type_shared, $type);
		push(@founded_filename_shared, $filename);
		push(@founded_dirpath_shared, $dirpath);
		push(@founded_filepath_shared, $filepath);
		push(@founded_created_shared, $stat[10]);
		push(@founded_modified_shared, $stat[9]);
		# Вот тут сейчас все блокировки из этого блока будут сняты.
	}, @ARGV);

	# File::Find закончил работу, чтобы это показать,
	# мы ставим $cur_dir_shared в undef.
	lock($cur_dir_shared);
	$cur_dir_shared = undef;
}


# Функция для конвертирования значения типа в строку.
sub type_id_to_text {
	my $value = shift();
	return("Файл") if($value == 0);
	return("Директория") if($value == 1);
}

К Thunar'у это легко прикручивается синей изолентой через "Особые действия".

PS: если вам помог данный пример в разработке своего приложения, или же вы его используете по прямому назначению (файлы ищите им), то напишите об этом комментариях. Мне интересно же... Даже, наверное, если оно окажется нужным не только мне, то подумаю об размещении где-нибудь на гитхабе и об расширении функционала.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

1 × 6 =