623 lines
14 KiB
C
623 lines
14 KiB
C
/* recutl.c - Common code for the utilities. */
|
|
|
|
/* Copyright (C) 2010-2022 Jose E. Marchesi */
|
|
|
|
/* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <stdlib.h>
|
|
#include <getopt.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <closeout.h>
|
|
#include <xalloc.h>
|
|
#include <unistd.h>
|
|
#include <locale.h>
|
|
#include <gettext.h>
|
|
#define _(str) gettext (str)
|
|
#if defined REC_CRYPT_SUPPORT
|
|
# include <gcrypt.h>
|
|
#endif
|
|
#include <progname.h>
|
|
#include <sys/stat.h>
|
|
#include <readline.h>
|
|
#include <regex.h>
|
|
#include <stdint.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <getpass.h>
|
|
#include <tmpdir.h>
|
|
#include <pathmax.h>
|
|
/* GNU/Hurd doesn't define PATH_MAX */
|
|
#ifndef PATH_MAX
|
|
# define PATH_MAX 8192
|
|
#endif
|
|
|
|
#include <rec.h>
|
|
#include <recutl.h>
|
|
#include "read-file.h"
|
|
#include "copy-file.h"
|
|
|
|
/*
|
|
* Global variables.
|
|
*/
|
|
|
|
static bool recutl_sort_p = false;
|
|
static char *recutl_order_rset = NULL;
|
|
static char *recutl_order_by_field = NULL;
|
|
static bool recutl_interactive_p = false;
|
|
static size_t *recutl_indexes = NULL;
|
|
static size_t recutl_indexes_size = 0;
|
|
|
|
void recutl_print_help (void); /* Forward prototype. */
|
|
|
|
void
|
|
recutl_init (char *util_name)
|
|
{
|
|
set_program_name (xstrdup (util_name));
|
|
|
|
#if defined REC_CRYPT_SUPPORT
|
|
/* Initialize libgcrypt */
|
|
gcry_check_version (NULL);
|
|
gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
|
|
#endif
|
|
|
|
/* Initialize librec */
|
|
rec_init ();
|
|
|
|
/* Even exiting has subtleties. On exit, if any writes failed, change
|
|
the exit status. The /dev/full device on GNU/Linux can be used for
|
|
testing; for instance, hello >/dev/full should exit unsuccessfully.
|
|
This is implemented in the Gnulib module "closeout". */
|
|
atexit (close_stdout);
|
|
|
|
/* i18n */
|
|
setlocale (LC_ALL, "");
|
|
bindtextdomain (PACKAGE, LOCALEDIR);
|
|
textdomain (PACKAGE);
|
|
|
|
/* Detect whether the tool has been invoked interactively. */
|
|
recutl_interactive_p = isatty (fileno(stdin));
|
|
|
|
/* Initially there are no indexes. */
|
|
recutl_reset_indexes ();
|
|
}
|
|
|
|
bool
|
|
recutl_interactive (void)
|
|
{
|
|
return recutl_interactive_p;
|
|
}
|
|
|
|
void
|
|
recutl_print_help_footer (void)
|
|
{
|
|
/* TRANSLATORS: --help output 5+ (reports)
|
|
TRANSLATORS: the placeholder indicates the bug-reporting address
|
|
for this application. Please add _another line_ with the
|
|
address for translation bugs.
|
|
no-wrap */
|
|
printf (_("\
|
|
Report bugs to: %s\n"), PACKAGE_BUGREPORT);
|
|
#ifdef PACKAGE_PACKAGER_BUG_REPORTS
|
|
printf (_("Report %s bugs to: %s\n"), PACKAGE_PACKAGER,
|
|
PACKAGE_PACKAGER_BUG_REPORTS);
|
|
#endif
|
|
#ifdef PACKAGE_URL
|
|
printf (_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
|
|
#else
|
|
printf (_("%s home page: <http://www.gnu.org/software/recutils/>\n"),
|
|
PACKAGE_NAME, PACKAGE);
|
|
#endif
|
|
fputs (_("General help using GNU software: <http://www.gnu.org/gethelp/>\n"),
|
|
stdout);
|
|
}
|
|
|
|
void
|
|
recutl_print_help_common (void)
|
|
{
|
|
/* TRANSLATORS: --help output, common arguments.
|
|
no-wrap */
|
|
fputs (_("\
|
|
--help print a help message and exit.\n\
|
|
--version show version and exit.\n"),
|
|
stdout);
|
|
}
|
|
|
|
void
|
|
recutl_print_help_record_selection (void)
|
|
{
|
|
/* TRANSLATORS: --help output, record selection arguments
|
|
no-wrap */
|
|
fputs (_("\
|
|
Record selection options:\n\
|
|
-i, --case-insensitive make strings case-insensitive in selection\n\
|
|
expressions.\n\
|
|
-t, --type=TYPE operate on records of the specified type only.\n\
|
|
-e, --expression=RECORD_EXPR selection expression.\n\
|
|
-q, --quick=STR select records with fields containing a string.\n\
|
|
-n, --number=NUM,... select specific records by position, with ranges.\n\
|
|
-m, --random=NUM select a given number of random records.\n"),
|
|
stdout);
|
|
}
|
|
|
|
void
|
|
recutl_print_version (void)
|
|
{
|
|
printf ("%s (GNU %s) %s\n",
|
|
program_name,
|
|
PACKAGE,
|
|
VERSION);
|
|
/* xgettext: no-wrap */
|
|
puts ("");
|
|
|
|
/* It is important to separate the year from the rest of the message,
|
|
as done here, to avoid having to retranslate the message when a new
|
|
year comes around. */
|
|
printf (_("\
|
|
Copyright (C) %s Jose E. Marchesi.\n\
|
|
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n\
|
|
This is free software: you are free to change and redistribute it.\n\
|
|
There is NO WARRANTY, to the extent permitted by law.\n"), "2010-2020");
|
|
|
|
puts (_("\
|
|
\n\
|
|
Written by Jose E. Marchesi."));
|
|
}
|
|
|
|
void
|
|
recutl_fatal (const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start (ap, fmt);
|
|
fputs (program_name, stderr);
|
|
fputs (_(": error: "), stderr);
|
|
vfprintf (stderr, fmt, ap);
|
|
va_end (ap);
|
|
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
void
|
|
recutl_out_of_memory (void)
|
|
{
|
|
recutl_fatal (_("out of memory\n"));
|
|
}
|
|
|
|
void
|
|
recutl_error (const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start (ap, fmt);
|
|
fputs (program_name, stderr);
|
|
fputs (_(": error: "), stderr);
|
|
vfprintf (stderr, fmt, ap);
|
|
va_end (ap);
|
|
}
|
|
|
|
void
|
|
recutl_warning (const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
va_start (ap, fmt);
|
|
fputs (program_name, stderr);
|
|
fputs (_(": warning: "), stderr);
|
|
vfprintf (stderr, fmt, ap);
|
|
va_end (ap);
|
|
}
|
|
|
|
bool
|
|
recutl_parse_db_from_file (FILE *in,
|
|
char *file_name,
|
|
rec_db_t db)
|
|
{
|
|
bool res;
|
|
rec_rset_t rset;
|
|
rec_parser_t parser;
|
|
|
|
res = true;
|
|
|
|
parser = rec_parser_new (in, file_name);
|
|
while (rec_parse_rset (parser, &rset))
|
|
{
|
|
char *rset_type;
|
|
/* XXX: check for consistency!!!. */
|
|
rset_type = rec_rset_type (rset);
|
|
if (rec_db_type_p (db, rset_type))
|
|
{
|
|
if (rset_type)
|
|
recutl_fatal (_("duplicated record set '%s' from %s.\n"),
|
|
rset_type, file_name);
|
|
else
|
|
{
|
|
/* Special case: the database already contains anonymous
|
|
records (with no type) and the record set to be
|
|
inserted also contains anonyous records. In this
|
|
case we just append the records and comments in the
|
|
anonymous record set. */
|
|
|
|
rec_rset_t anon_rset = rec_db_get_rset_by_type (db, NULL);
|
|
rec_mset_iterator_t iter = rec_mset_iterator (rec_rset_mset (rset));
|
|
rec_mset_elem_t elem;
|
|
while (rec_mset_iterator_next (&iter, MSET_ANY, NULL, &elem))
|
|
{
|
|
void *data = rec_mset_elem_dup_data (elem);
|
|
if (!data
|
|
|| !rec_mset_append (rec_rset_mset (anon_rset),
|
|
rec_mset_elem_type (elem),
|
|
data,
|
|
MSET_ANY))
|
|
return false;
|
|
}
|
|
rec_mset_iterator_free (&iter);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!rec_db_insert_rset (db, rset, rec_db_size (db)))
|
|
{
|
|
/* Error. */
|
|
res = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (rec_parser_error (parser))
|
|
{
|
|
/* Report parsing errors. */
|
|
rec_parser_perror (parser, "%s", file_name);
|
|
res = false;
|
|
}
|
|
rec_parser_destroy (parser);
|
|
|
|
return res;
|
|
}
|
|
|
|
rec_db_t
|
|
recutl_build_db (int argc, char **argv)
|
|
{
|
|
rec_db_t db;
|
|
char *file_name;
|
|
FILE *in;
|
|
|
|
db = rec_db_new ();
|
|
if (!db)
|
|
return NULL;
|
|
|
|
/* Register the default functions in the database. */
|
|
|
|
|
|
|
|
/* Process the input files, if any. Otherwise use the standard
|
|
input to read the rec data. */
|
|
|
|
if (optind < argc)
|
|
{
|
|
while (optind < argc)
|
|
{
|
|
file_name = argv[optind++];
|
|
if (!(in = fopen (file_name, "r")))
|
|
recutl_fatal (_("cannot read file %s\n"), file_name);
|
|
else
|
|
{
|
|
if (!recutl_parse_db_from_file (in, file_name, db))
|
|
{
|
|
free (db);
|
|
db = NULL;
|
|
}
|
|
|
|
fclose (in);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!recutl_parse_db_from_file (stdin, "stdin", db))
|
|
{
|
|
free (db);
|
|
db = NULL;
|
|
}
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
rec_db_t
|
|
recutl_read_db_from_file (char *file_name)
|
|
{
|
|
rec_db_t db;
|
|
FILE *in;
|
|
|
|
db = rec_db_new ();
|
|
if (file_name)
|
|
{
|
|
in = fopen (file_name, "r");
|
|
if (in == NULL)
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
/* Process the standard input. */
|
|
file_name = "stdin";
|
|
in = stdin;
|
|
}
|
|
|
|
if (!recutl_parse_db_from_file (in,
|
|
file_name,
|
|
db))
|
|
{
|
|
rec_db_destroy (db);
|
|
db = NULL;
|
|
}
|
|
|
|
return db;
|
|
}
|
|
|
|
bool
|
|
recutl_file_is_writable (char *file_name)
|
|
{
|
|
return !file_name || (euidaccess (file_name, W_OK) == 0);
|
|
}
|
|
|
|
void
|
|
recutl_write_db_to_file (rec_db_t db,
|
|
char *file_name)
|
|
{
|
|
FILE *out;
|
|
char tmp_file_name[PATH_MAX];
|
|
rec_writer_t writer;
|
|
int des;
|
|
struct stat st1;
|
|
int stat_result;
|
|
|
|
if (!file_name)
|
|
out = stdout;
|
|
else
|
|
{
|
|
/* Record the original file attributes. */
|
|
stat_result = stat (file_name, &st1);
|
|
|
|
/* Create a temporary file with the results. */
|
|
if ((path_search (tmp_file_name, PATH_MAX, NULL,
|
|
"rec", true) == -1)
|
|
|| ((des = mkstemp (tmp_file_name)) == -1))
|
|
recutl_fatal (_("cannot create a unique name.\n"));
|
|
|
|
out = fdopen (des, "w+");
|
|
}
|
|
|
|
writer = rec_writer_new (out);
|
|
rec_write_db (writer, db);
|
|
|
|
if (file_name)
|
|
fclose (out);
|
|
|
|
rec_db_destroy (db);
|
|
|
|
if (file_name)
|
|
{
|
|
/* Rename the temporary file to file_name. We copy and remove
|
|
instead of renaming because the later doesn't work across
|
|
different mount points, and it is getting common for /tmp to
|
|
be mounted in its own filesystem. */
|
|
if (qcopy_file_preserving (tmp_file_name, file_name) != 0)
|
|
recutl_fatal (_("renaming file %s to %s\n"), tmp_file_name, file_name);
|
|
remove (tmp_file_name);
|
|
|
|
/* Restore the attributes of the original file. */
|
|
if (stat_result != -1)
|
|
chmod (file_name, st1.st_mode);
|
|
}
|
|
}
|
|
|
|
char *
|
|
recutl_read_file (char *file_name)
|
|
{
|
|
char *result;
|
|
FILE *in;
|
|
|
|
result = NULL;
|
|
in = fopen (file_name, "r");
|
|
if (in)
|
|
{
|
|
size_t file_size;
|
|
result = fread_file (in, 0, &file_size);
|
|
fclose (in);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void
|
|
recutl_check_integrity (rec_db_t db,
|
|
bool verbose_p,
|
|
bool external_p)
|
|
{
|
|
rec_buf_t errors_buf;
|
|
char *errors_str;
|
|
size_t errors_str_size;
|
|
|
|
errors_buf = rec_buf_new (&errors_str, &errors_str_size);
|
|
if (rec_int_check_db (db, true, external_p, errors_buf) > 0)
|
|
{
|
|
rec_buf_close (errors_buf);
|
|
if (!verbose_p)
|
|
{
|
|
recutl_error (_("operation aborted due to integrity failures.\n"));
|
|
recutl_error (_("use --verbose to get a detailed report.\n"));
|
|
}
|
|
else
|
|
{
|
|
fprintf (stderr, "%s", errors_str);
|
|
}
|
|
|
|
recutl_fatal (_("use --force to skip the integrity check.\n"));
|
|
}
|
|
}
|
|
|
|
bool
|
|
recutl_yesno (char *prompt)
|
|
{
|
|
bool res = false;
|
|
char *line = NULL;
|
|
|
|
while (1)
|
|
{
|
|
line = readline (prompt);
|
|
if (line)
|
|
{
|
|
if (strcmp (line, "yes") == 0)
|
|
{
|
|
res = true;
|
|
break;
|
|
}
|
|
else if (strcmp (line, "no") == 0)
|
|
{
|
|
res = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
printf ("Please answer 'yes' or 'no'.\n");
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
#define INDEX_LIST_ENTRY_RE "[0-9]+(-[0-9]+)?"
|
|
#define INDEX_LIST_RE "(" INDEX_LIST_ENTRY_RE ",)*" INDEX_LIST_ENTRY_RE
|
|
|
|
bool
|
|
recutl_index_list_parse (const char *str)
|
|
{
|
|
regex_t regexp;
|
|
bool res = true;
|
|
const char *p;
|
|
long int number;
|
|
char *end;
|
|
size_t i;
|
|
|
|
/* Initialize the list structure. An pessimistic estimation of the
|
|
number of indexes encoded in the string is used. */
|
|
|
|
free (recutl_indexes);
|
|
recutl_indexes = xmalloc (sizeof (size_t) * (strlen (str) * 2 + 2));
|
|
for (i = 0; i < (strlen (str) * 2 + 2); i++)
|
|
recutl_indexes[i] = REC_Q_NOINDEX;
|
|
|
|
/* Make sure the string is valid. The code below relies on this
|
|
fact. */
|
|
if (regcomp (®exp, "^" INDEX_LIST_RE "$", REG_EXTENDED) != 0)
|
|
{
|
|
recutl_fatal (_("internal error: recutl_index_list_parse: error compiling regexp.\n"));
|
|
return false;
|
|
}
|
|
|
|
if (regexec (®exp, str, 0, NULL, 0) != 0)
|
|
{
|
|
regfree (®exp);
|
|
return false;
|
|
}
|
|
|
|
regfree (®exp);
|
|
|
|
/* Parse the string. */
|
|
p = str;
|
|
while (true)
|
|
{
|
|
/* Get the 'min' part of the entry. */
|
|
number = strtol (p, &end, 10);
|
|
recutl_indexes[recutl_indexes_size] = (size_t) number;
|
|
p = end;
|
|
|
|
/* Get the 'max' part of the entry, if any. */
|
|
if (*p == '-')
|
|
{
|
|
p++;
|
|
number = strtol (p, &end, 10);
|
|
recutl_indexes[recutl_indexes_size+1] = (size_t) number;
|
|
p = end;
|
|
}
|
|
|
|
recutl_indexes_size = recutl_indexes_size + 2;
|
|
|
|
/* Exit or pass the separator. */
|
|
if (*p == '\0')
|
|
break;
|
|
else
|
|
p++;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void
|
|
recutl_reset_indexes (void)
|
|
{
|
|
free (recutl_indexes);
|
|
recutl_indexes = NULL;
|
|
recutl_indexes_size = 0;
|
|
}
|
|
|
|
size_t
|
|
recutl_num_indexes (void)
|
|
{
|
|
return recutl_indexes_size;
|
|
}
|
|
|
|
size_t *
|
|
recutl_index (void)
|
|
{
|
|
return recutl_indexes;
|
|
}
|
|
|
|
char *
|
|
recutl_getpass (bool asktwice)
|
|
{
|
|
char *ret = NULL;
|
|
|
|
char *pass = getpass (_("Password: "));
|
|
if (pass)
|
|
{
|
|
ret = xstrdup (pass);
|
|
if (asktwice)
|
|
{
|
|
pass = getpass (_("Password again: "));
|
|
if (pass)
|
|
{
|
|
if (strcmp (ret, pass) != 0)
|
|
{
|
|
recutl_fatal (_("the provided passwords don't match.\n"));
|
|
memset (ret, 0, strlen (ret));
|
|
memset (pass, 0, strlen (pass));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
memset (ret, 0, strlen (ret));
|
|
free (ret);
|
|
ret = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|