mirror of
https://github.com/fmang/opustags.git
synced 2025-07-07 10:04:30 +02:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a1b8705cc | |||
7c8396ca45 | |||
639d46ed0f | |||
d54bada7e6 | |||
57a4c0d5a0 | |||
d071b6cabd | |||
d8c36a3d3f | |||
ba2236facb | |||
b3b092d241 | |||
8f0f29c056 | |||
e4ca6ca6ef | |||
df03cdf951 | |||
8252f94084 | |||
a1dcc8c47e | |||
7206604f85 | |||
6da5545b30 | |||
537094fd53 | |||
be9740fe05 |
@ -1,6 +1,11 @@
|
||||
opustags changelog
|
||||
==================
|
||||
|
||||
1.5.0 - 2020-11-08
|
||||
------------------
|
||||
|
||||
- Introduce --edit for interactive edition.
|
||||
|
||||
1.4.0 - 2020-10-04
|
||||
------------------
|
||||
|
||||
|
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.9)
|
||||
|
||||
project(
|
||||
opustags
|
||||
VERSION 1.4.0
|
||||
VERSION 1.5.0
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
@ -19,8 +19,10 @@ pkg_check_modules(OGG REQUIRED ogg)
|
||||
add_compile_options(${OGG_CFLAGS})
|
||||
link_directories(${OGG_LIBRARY_DIRS})
|
||||
|
||||
include(FindIconv)
|
||||
|
||||
configure_file(src/config.h.in config.h @ONLY)
|
||||
include_directories(BEFORE src "${CMAKE_BINARY_DIR}" ${OGG_INCLUDE_DIRS})
|
||||
include_directories(BEFORE src "${CMAKE_BINARY_DIR}" ${OGG_INCLUDE_DIRS} ${Iconv_INCLUDE_DIRS})
|
||||
|
||||
add_library(
|
||||
ot
|
||||
@ -30,11 +32,7 @@ add_library(
|
||||
src/opus.cc
|
||||
src/system.cc
|
||||
)
|
||||
target_link_libraries(ot PUBLIC ${OGG_LIBRARIES})
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(ot PUBLIC iconv)
|
||||
endif()
|
||||
target_link_libraries(ot PUBLIC ${OGG_LIBRARIES} ${Iconv_LIBRARIES})
|
||||
|
||||
add_executable(opustags src/opustags.cc)
|
||||
target_link_libraries(opustags ot)
|
||||
|
@ -61,5 +61,6 @@ Documentation
|
||||
-D, --delete-all delete all the previously existing comments
|
||||
-s, --set FIELD=VALUE replace a comment
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
|
||||
See the man page, `opustags.1`, for extensive documentation.
|
||||
|
11
opustags.1
11
opustags.1
@ -97,7 +97,12 @@ Delete all the previously existing tags.
|
||||
Sets the tags from scratch.
|
||||
All the original tags are deleted and new ones are read from standard input.
|
||||
Each line must specify a \fIFIELD=VALUE\fP pair and be separated with line feeds.
|
||||
Blank lines are ignored.
|
||||
Blank lines and lines starting with \fI#\fP are ignored.
|
||||
.TP
|
||||
.B \-e, \-\-edit
|
||||
Edit tags interactively by spawning the program specified by the EDITOR
|
||||
environment variable. The allowed format is the same as \fB--set-all\fP.
|
||||
If TERM and VISUAL are set, VISUAL takes precedence over EDITOR.
|
||||
.SH EXAMPLES
|
||||
.PP
|
||||
List all the tags in file foo.opus:
|
||||
@ -116,6 +121,10 @@ Remove the previously existing ARTIST tags and add the two X and Y ARTIST tags,
|
||||
tags without writing them to the Opus file:
|
||||
.PP
|
||||
opustags in.opus --add ARTIST=X --add ARTIST=Y --delete ARTIST
|
||||
.PP
|
||||
Edit tags interactively in Vim:
|
||||
.PP
|
||||
EDITOR=vim opustags --in-place --edit file.opus
|
||||
.SH CAVEATS
|
||||
.PP
|
||||
\fBopustags\fP currently has the following limitations:
|
||||
|
136
src/cli.cc
136
src/cli.cc
@ -36,6 +36,7 @@ Options:
|
||||
-D, --delete-all delete all the previously existing comments
|
||||
-s, --set FIELD=VALUE replace a comment
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
|
||||
See the man page for extensive documentation.
|
||||
)raw";
|
||||
@ -50,6 +51,7 @@ static struct option getopt_options[] = {
|
||||
{"set", required_argument, 0, 's'},
|
||||
{"delete-all", no_argument, 0, 'D'},
|
||||
{"set-all", no_argument, 0, 'S'},
|
||||
{"edit", no_argument, 0, 'e'},
|
||||
{NULL, 0, 0, 0}
|
||||
};
|
||||
|
||||
@ -65,7 +67,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
|
||||
return {st::bad_arguments, "No arguments specified. Use -h for help."};
|
||||
int c;
|
||||
optind = 0;
|
||||
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DS", getopt_options, NULL)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSe", getopt_options, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'h':
|
||||
opt.print_help = true;
|
||||
@ -77,6 +79,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
|
||||
break;
|
||||
case 'i':
|
||||
opt.in_place = true;
|
||||
opt.overwrite = true;
|
||||
break;
|
||||
case 'y':
|
||||
opt.overwrite = true;
|
||||
@ -105,6 +108,9 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
|
||||
case 'D':
|
||||
opt.delete_all = true;
|
||||
break;
|
||||
case 'e':
|
||||
opt.edit_interactively = true;
|
||||
break;
|
||||
case ':':
|
||||
return {st::bad_arguments,
|
||||
"Missing value for option '"s + argv[optind - 1] + "'."};
|
||||
@ -115,32 +121,42 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
|
||||
}
|
||||
if (opt.print_help)
|
||||
return st::ok;
|
||||
if (opt.in_place) {
|
||||
if (opt.path_out)
|
||||
return {st::bad_arguments, "Cannot combine --in-place and --output."};
|
||||
opt.overwrite = true;
|
||||
for (int i = optind; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-") == 0)
|
||||
return {st::bad_arguments, "Cannot modify standard input in place."};
|
||||
opt.paths_in.emplace_back(argv[i]);
|
||||
}
|
||||
} else {
|
||||
if (optind != argc - 1)
|
||||
return {st::bad_arguments, "Exactly one input file must be specified."};
|
||||
if (set_all && strcmp(argv[optind], "-") == 0)
|
||||
return {st::bad_arguments,
|
||||
"Cannot use standard input as input file when --set-all is specified."};
|
||||
opt.paths_in.emplace_back(argv[optind]);
|
||||
|
||||
// All non-option arguments are input files.
|
||||
bool stdin_as_input = false;
|
||||
for (int i = optind; i < argc; i++) {
|
||||
stdin_as_input = stdin_as_input || strcmp(argv[i], "-") == 0;
|
||||
opt.paths_in.emplace_back(argv[i]);
|
||||
}
|
||||
|
||||
if (opt.in_place && opt.path_out)
|
||||
return {st::bad_arguments, "Cannot combine --in-place and --output."};
|
||||
|
||||
if (opt.in_place && stdin_as_input)
|
||||
return {st::bad_arguments, "Cannot modify standard input in place."};
|
||||
|
||||
if ((!opt.in_place || opt.edit_interactively) && opt.paths_in.size() != 1)
|
||||
return {st::bad_arguments, "Exactly one input file must be specified."};
|
||||
|
||||
if (set_all && stdin_as_input)
|
||||
return {st::bad_arguments, "Cannot use standard input as input file when --set-all is specified."};
|
||||
|
||||
if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-"))
|
||||
return {st::bad_arguments, "Cannot edit interactively when standard input or standard output are already used."};
|
||||
|
||||
if (opt.edit_interactively && !opt.path_out.has_value() && !opt.in_place)
|
||||
return {st::bad_arguments, "Cannot edit interactively when no output is specified."};
|
||||
|
||||
if (opt.edit_interactively && (opt.delete_all || !opt.to_add.empty() || !opt.to_delete.empty()))
|
||||
return {st::bad_arguments, "Cannot mix --edit with -adDsS."};
|
||||
|
||||
if (set_all) {
|
||||
// Read comments from stdin and prepend them to opt.to_add.
|
||||
std::vector<std::string> comments;
|
||||
std::list<std::string> comments;
|
||||
auto rc = read_comments(comments_input, comments);
|
||||
if (rc != st::ok)
|
||||
return rc;
|
||||
comments.reserve(comments.size() + opt.to_add.size());
|
||||
std::move(opt.to_add.begin(), opt.to_add.end(), std::back_inserter(comments));
|
||||
opt.to_add = std::move(comments);
|
||||
opt.to_add.splice(opt.to_add.begin(), std::move(comments));
|
||||
}
|
||||
return st::ok;
|
||||
}
|
||||
@ -175,7 +191,7 @@ void ot::print_comments(const std::list<std::string>& comments, FILE* output)
|
||||
has_control = true;
|
||||
}
|
||||
fwrite(local.data(), 1, local.size(), output);
|
||||
putchar('\n');
|
||||
putc('\n', output);
|
||||
}
|
||||
if (info_lost)
|
||||
fputs("warning: Some tags have been transliterated to your system encoding.\n", stderr);
|
||||
@ -188,7 +204,7 @@ void ot::print_comments(const std::list<std::string>& comments, FILE* output)
|
||||
fputs("warning: Some tags contain control characters.\n", stderr);
|
||||
}
|
||||
|
||||
ot::status ot::read_comments(FILE* input, std::vector<std::string>& comments)
|
||||
ot::status ot::read_comments(FILE* input, std::list<std::string>& comments)
|
||||
{
|
||||
static ot::encoding_converter to_utf8("", "UTF-8");
|
||||
comments.clear();
|
||||
@ -200,6 +216,8 @@ ot::status ot::read_comments(FILE* input, std::vector<std::string>& comments)
|
||||
--nread;
|
||||
if (nread == 0)
|
||||
continue;
|
||||
if (line[0] == '#') // comment
|
||||
continue;
|
||||
if (memchr(line, '=', nread) == nullptr) {
|
||||
ot::status rc = {ot::st::error, "Malformed tag: " + std::string(line, nread)};
|
||||
free(line);
|
||||
@ -256,6 +274,68 @@ static ot::status edit_tags(ot::opus_tags& tags, const ot::options& opt)
|
||||
return ot::st::ok;
|
||||
}
|
||||
|
||||
/** Spawn VISUAL or EDITOR to edit the given tags. */
|
||||
static ot::status edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path)
|
||||
{
|
||||
const char* editor = nullptr;
|
||||
if (getenv("TERM") != nullptr)
|
||||
editor = getenv("VISUAL");
|
||||
if (editor == nullptr) // without a terminal, or if VISUAL is unset
|
||||
editor = getenv("EDITOR");
|
||||
if (editor == nullptr)
|
||||
return {ot::st::error,
|
||||
"No editor specified in environment variable VISUAL or EDITOR."};
|
||||
|
||||
// Building the temporary tags file.
|
||||
std::string tags_path = base_path.value_or("tags") + ".XXXXXX.opustags";
|
||||
int fd = mkstemps(const_cast<char*>(tags_path.data()), 9);
|
||||
FILE* tags_file;
|
||||
if (fd == -1 || (tags_file = fdopen(fd, "w")) == nullptr)
|
||||
return {ot::st::standard_error,
|
||||
"Could not open '" + tags_path + "': " + strerror(errno)};
|
||||
ot::print_comments(tags.comments, tags_file);
|
||||
if (fclose(tags_file) != 0)
|
||||
return {ot::st::standard_error, tags_path + ": fclose error: "s + strerror(errno)};
|
||||
|
||||
// Spawn the editor, and watch the modification timestamps.
|
||||
ot::status rc;
|
||||
timespec before, after;
|
||||
if ((rc = ot::get_file_timestamp(tags_path.c_str(), before)) != ot::st::ok)
|
||||
return rc;
|
||||
ot::status editor_rc = ot::run_editor(editor, tags_path);
|
||||
if ((rc = ot::get_file_timestamp(tags_path.c_str(), after)) != ot::st::ok)
|
||||
return rc; // probably because the file was deleted
|
||||
bool modified = (before.tv_sec != after.tv_sec || before.tv_nsec != after.tv_nsec);
|
||||
if (editor_rc != ot::st::ok) {
|
||||
if (modified)
|
||||
fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str());
|
||||
else
|
||||
remove(tags_path.c_str());
|
||||
return editor_rc;
|
||||
} else if (!modified) {
|
||||
remove(tags_path.c_str());
|
||||
fputs("Cancelling edition because the tags file was not modified.\n", stderr);
|
||||
return ot::st::cancel;
|
||||
}
|
||||
|
||||
// Applying the new tags.
|
||||
tags_file = fopen(tags_path.c_str(), "re");
|
||||
if (tags_file == nullptr)
|
||||
return {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)};
|
||||
if ((rc = ot::read_comments(tags_file, tags.comments)) != ot::st::ok) {
|
||||
fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str());
|
||||
return rc;
|
||||
}
|
||||
fclose(tags_file);
|
||||
|
||||
// Remove the temporary tags file only on success, because unlike the
|
||||
// partial Ogg file that is irrecoverable, the edited tags file
|
||||
// contains user data, so let’s leave users a chance to recover it.
|
||||
remove(tags_path.c_str());
|
||||
|
||||
return ot::st::ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop of opustags. Read the packets from the reader, and forwards them to the writer.
|
||||
* Transform the OpusTags packet on the fly.
|
||||
@ -302,6 +382,11 @@ static ot::status process(ot::ogg_reader& reader, ot::ogg_writer* writer, const
|
||||
if ((rc = edit_tags(tags, opt)) != ot::st::ok)
|
||||
return rc;
|
||||
if (writer) {
|
||||
if (opt.edit_interactively) {
|
||||
fflush(writer->file); // flush before calling the subprocess
|
||||
if ((rc = edit_tags_interactively(tags, writer->path)) != ot::st::ok)
|
||||
return rc;
|
||||
}
|
||||
auto packet = ot::render_tags(tags);
|
||||
rc = writer->write_header_packet(serialno, pageno, packet);
|
||||
if (rc != ot::st::ok)
|
||||
@ -325,7 +410,7 @@ static ot::status run_single(const ot::options& opt, const std::string& path_in,
|
||||
ot::file input;
|
||||
if (path_in == "-")
|
||||
input = stdin;
|
||||
else if ((input = fopen(path_in.c_str(), "r")) == nullptr)
|
||||
else if ((input = fopen(path_in.c_str(), "re")) == nullptr)
|
||||
return {ot::st::standard_error,
|
||||
"Could not open '" + path_in + "' for reading: " + strerror(errno)};
|
||||
ot::ogg_reader reader(input.get());
|
||||
@ -365,7 +450,7 @@ static ot::status run_single(const ot::options& opt, const std::string& path_in,
|
||||
/* The output file exists. */
|
||||
if (!S_ISREG(output_info.st_mode)) {
|
||||
/* Special files are opened for writing directly. */
|
||||
if ((final_output = fopen(path_out->c_str(), "w")) == nullptr)
|
||||
if ((final_output = fopen(path_out->c_str(), "we")) == nullptr)
|
||||
rc = {ot::st::standard_error,
|
||||
"Could not open '" + path_out.value() + "' for writing: " +
|
||||
strerror(errno)};
|
||||
@ -388,6 +473,7 @@ static ot::status run_single(const ot::options& opt, const std::string& path_in,
|
||||
return rc;
|
||||
|
||||
ot::ogg_writer writer(output);
|
||||
writer.path = path_out;
|
||||
rc = process(reader, &writer, opt);
|
||||
if (rc == ot::st::ok)
|
||||
rc = temporary_output.commit();
|
||||
|
@ -27,11 +27,14 @@
|
||||
#include <iconv.h>
|
||||
#include <ogg/ogg.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ot {
|
||||
@ -57,9 +60,11 @@ enum class st {
|
||||
error,
|
||||
standard_error, /**< Error raised by the C standard library. */
|
||||
int_overflow,
|
||||
cancel,
|
||||
/* System */
|
||||
badly_encoded,
|
||||
information_lost,
|
||||
child_process_failed,
|
||||
/* Ogg */
|
||||
bad_stream,
|
||||
end_of_stream,
|
||||
@ -161,6 +166,24 @@ private:
|
||||
iconv_t cd; /**< conversion descriptor */
|
||||
};
|
||||
|
||||
/** Escape a string so that a POSIX shell interprets it as a single argument. */
|
||||
std::string shell_escape(std::string_view word);
|
||||
|
||||
/**
|
||||
* Execute the editor process specified in editor. Wait for the process to exit and
|
||||
* return st::ok on success, or st::child_process_failed if it did not exit with 0.
|
||||
*
|
||||
* editor is passed unescaped to the shell, and may contain CLI options.
|
||||
* path is the name of the file to edit, which will be passed as the last argument to editor.
|
||||
*/
|
||||
ot::status run_editor(std::string_view editor, std::string_view path);
|
||||
|
||||
/**
|
||||
* Return the specified path’s mtime, i.e. the last data modification
|
||||
* timestamp.
|
||||
*/
|
||||
ot::status get_file_timestamp(const char* path, timespec& mtime);
|
||||
|
||||
/** \} */
|
||||
|
||||
/***********************************************************************************************//**
|
||||
@ -282,6 +305,10 @@ struct ogg_writer {
|
||||
* represented as a block of data and a length.
|
||||
*/
|
||||
FILE* file;
|
||||
/**
|
||||
* Path to the output file.
|
||||
*/
|
||||
std::optional<std::string> path;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -401,6 +428,15 @@ struct options {
|
||||
* Options: --in-place
|
||||
*/
|
||||
bool in_place = false;
|
||||
/**
|
||||
* Spawn EDITOR to edit tags interactively.
|
||||
*
|
||||
* stdin and stdout must be left free for the editor, so paths_in and
|
||||
* path_out can’t take `-`, and --set-all is not supported.
|
||||
*
|
||||
* Option: --edit
|
||||
*/
|
||||
bool edit_interactively = false;
|
||||
/**
|
||||
* List of comments to delete. Each string is a selector according to the definition of
|
||||
* #delete_comments.
|
||||
@ -429,7 +465,7 @@ struct options {
|
||||
*
|
||||
* Options: --add, --set, --set-all
|
||||
*/
|
||||
std::vector<std::string> to_add;
|
||||
std::list<std::string> to_add;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -455,7 +491,7 @@ void print_comments(const std::list<std::string>& comments, FILE* output);
|
||||
*
|
||||
* The comments are converted from the system encoding to UTF-8, and returned as UTF-8.
|
||||
*/
|
||||
status read_comments(FILE* input, std::vector<std::string>& comments);
|
||||
status read_comments(FILE* input, std::list<std::string>& comments);
|
||||
|
||||
/**
|
||||
* Remove all comments matching the specified selector, which may either be a field name or a
|
||||
|
@ -12,10 +12,14 @@
|
||||
#include <opustags.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
ot::status ot::partial_file::open(const char* destination)
|
||||
{
|
||||
abort();
|
||||
@ -132,3 +136,49 @@ ot::status ot::encoding_converter::operator()(const char* in, size_t n, std::str
|
||||
"in string '" + std::string(in, n) + "'."};
|
||||
return ot::st::ok;
|
||||
}
|
||||
|
||||
std::string ot::shell_escape(std::string_view word)
|
||||
{
|
||||
std::string escaped_word;
|
||||
// Pre-allocate the result, assuming most of the time enclosing it in single quotes is enough.
|
||||
escaped_word.reserve(2 + word.size());
|
||||
|
||||
escaped_word += '\'';
|
||||
for (char c : word) {
|
||||
if (c == '\'')
|
||||
escaped_word += "'\\''";
|
||||
else if (c == '!')
|
||||
escaped_word += "'\\!'";
|
||||
else
|
||||
escaped_word += c;
|
||||
}
|
||||
escaped_word += '\'';
|
||||
|
||||
return escaped_word;
|
||||
}
|
||||
|
||||
ot::status ot::run_editor(std::string_view editor, std::string_view path)
|
||||
{
|
||||
std::string command = std::string(editor) + " " + shell_escape(path);
|
||||
int status = system(command.c_str());
|
||||
|
||||
if (status == -1)
|
||||
return {st::standard_error, "waitpid error: "s + strerror(errno)};
|
||||
else if (!WIFEXITED(status))
|
||||
return {st::child_process_failed,
|
||||
"Child process did not terminate normally: "s + strerror(errno)};
|
||||
else if (WEXITSTATUS(status) != 0)
|
||||
return {st::child_process_failed,
|
||||
"Child process exited with " + std::to_string(WEXITSTATUS(status))};
|
||||
|
||||
return st::ok;
|
||||
}
|
||||
|
||||
ot::status ot::get_file_timestamp(const char* path, timespec& mtime)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(path, &st) == -1)
|
||||
return {st::standard_error, path + ": stat error: "s + strerror(errno)};
|
||||
mtime = st.st_mtim; // more precise than st_mtime
|
||||
return st::ok;
|
||||
}
|
||||
|
24
t/cli.cc
24
t/cli.cc
@ -7,7 +7,7 @@ using namespace std::literals::string_literals;
|
||||
|
||||
void check_read_comments()
|
||||
{
|
||||
std::vector<std::string> comments;
|
||||
std::list<std::string> comments;
|
||||
ot::status rc;
|
||||
{
|
||||
std::string txt = "TITLE=a b c\n\nARTIST=X\nArtist=Y\n"s;
|
||||
@ -72,19 +72,24 @@ void check_good_arguments()
|
||||
if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || !opt.path_out ||
|
||||
opt.path_out != "y" || !opt.delete_all || opt.overwrite || opt.to_delete.size() != 2 ||
|
||||
opt.to_delete[0] != "X" || opt.to_delete[1] != "a=b" ||
|
||||
opt.to_add.size() != 1 || opt.to_add[0] != "X=Y Z")
|
||||
opt.to_add != std::list<std::string>{"X=Y Z"})
|
||||
throw failure("unexpected option parsing result for case #1");
|
||||
|
||||
opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
|
||||
if (opt.paths_in.size() != 1 || opt.paths_in.front() != "x" || opt.path_out ||
|
||||
!opt.overwrite || opt.to_delete.size() != 0 ||
|
||||
opt.to_add.size() != 2 || opt.to_add[0] != "N=1" || opt.to_add[1] != "x=y z")
|
||||
opt.to_add != std::list<std::string>{"N=1", "x=y z"})
|
||||
throw failure("unexpected option parsing result for case #2");
|
||||
|
||||
opt = parse({"opustags", "-i", "x", "y", "z"});
|
||||
if (opt.paths_in.size() != 3 || opt.paths_in[0] != "x" || opt.paths_in[1] != "y" ||
|
||||
opt.paths_in[2] != "z" || !opt.overwrite || !opt.in_place)
|
||||
throw failure("unexpected option parsing result for case #3");
|
||||
|
||||
opt = parse({"opustags", "-ie", "x"});
|
||||
if (opt.paths_in.size() != 1 || opt.paths_in[0] != "x" ||
|
||||
!opt.edit_interactively || !opt.overwrite || !opt.in_place)
|
||||
throw failure("unexpected option parsing result for case #4");
|
||||
}
|
||||
|
||||
void check_bad_arguments()
|
||||
@ -121,6 +126,19 @@ void check_bad_arguments()
|
||||
error_code_case({"opustags", "-S", "x"}, "Malformed tag: INVALID", ot::st::error, "attempt to read invalid argument with -S");
|
||||
error_case({"opustags", "-o", "", "--output", "y", "z"},
|
||||
"Cannot specify --output more than once.", "double output with first filename empty");
|
||||
error_case({"opustags", "-e", "-i", "x", "y"},
|
||||
"Exactly one input file must be specified.", "editing interactively two files at once");
|
||||
error_case({"opustags", "--edit", "-", "-o", "x"},
|
||||
"Cannot edit interactively when standard input or standard output are already used.",
|
||||
"editing interactively from stdandard intput");
|
||||
error_case({"opustags", "--edit", "x", "-o", "-"},
|
||||
"Cannot edit interactively when standard input or standard output are already used.",
|
||||
"editing interactively to stdandard output");
|
||||
error_case({"opustags", "--edit", "x"}, "Cannot edit interactively when no output is specified.", "editing without output");
|
||||
error_case({"opustags", "--edit", "x", "-i", "-a", "X=Y"}, "Cannot mix --edit with -adDsS.", "mixing -e and -a");
|
||||
error_case({"opustags", "--edit", "x", "-i", "-d", "X"}, "Cannot mix --edit with -adDsS.", "mixing -e and -d");
|
||||
error_case({"opustags", "--edit", "x", "-i", "-D"}, "Cannot mix --edit with -adDsS.", "mixing -e and -D");
|
||||
error_case({"opustags", "--edit", "x", "-i", "-S"}, "Cannot mix --edit with -adDsS.", "mixing -e and -S");
|
||||
}
|
||||
|
||||
static void check_delete_comments()
|
||||
|
21
t/opustags.t
21
t/opustags.t
@ -4,7 +4,7 @@ use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Test::More tests => 41;
|
||||
use Test::More tests => 47;
|
||||
|
||||
use Digest::MD5;
|
||||
use File::Basename;
|
||||
@ -71,6 +71,7 @@ Options:
|
||||
-D, --delete-all delete all the previously existing comments
|
||||
-s, --set FIELD=VALUE replace a comment
|
||||
-S, --set-all import comments from standard input
|
||||
-e, --edit edit tags interactively in VISUAL/EDITOR
|
||||
|
||||
See the man page for extensive documentation.
|
||||
EOF
|
||||
@ -179,6 +180,7 @@ ARTIST=七面鳥
|
||||
|
||||
A=A
|
||||
X=Y
|
||||
#IGNORE=COMMENTS
|
||||
END_IN
|
||||
OK=yes again
|
||||
ARTIST=七面鳥
|
||||
@ -221,6 +223,23 @@ is(md5('out2.opus'), '0a4d20c287b2e46b26cb0eee353c2069', 'the tags were added co
|
||||
unlink('out.opus');
|
||||
unlink('out2.opus');
|
||||
|
||||
####################################################################################################
|
||||
# Interactive edition
|
||||
|
||||
$ENV{EDITOR} = 'sed -i -e y/aeiou/AEIOU/ `sleep 0.1`';
|
||||
is_deeply(opustags('gobble.opus', '-eo', "'screaming !'.opus"), ['', '', 0], 'edit a file with EDITOR');
|
||||
is(md5("'screaming !'.opus"), '56e85ccaa83a13c15576d75bbd6d835f', 'the tags were modified');
|
||||
|
||||
$ENV{EDITOR} = 'true';
|
||||
is_deeply(opustags('-ie', "'screaming !'.opus"), ['', "Cancelling edition because the tags file was not modified.\n", 256], 'close -e without saving');
|
||||
is(md5("'screaming !'.opus"), '56e85ccaa83a13c15576d75bbd6d835f', 'the tags were not modified');
|
||||
|
||||
$ENV{EDITOR} = 'false';
|
||||
is_deeply(opustags('-ie', "'screaming !'.opus"), ['', "'screaming !'.opus: error: Child process exited with 1\n", 256], 'editor exiting with an error');
|
||||
is(md5("'screaming !'.opus"), '56e85ccaa83a13c15576d75bbd6d835f', 'the tags were not modified');
|
||||
|
||||
unlink("'screaming !'.opus");
|
||||
|
||||
####################################################################################################
|
||||
# Test muxed streams
|
||||
|
||||
|
11
t/system.cc
11
t/system.cc
@ -49,10 +49,19 @@ void check_converter()
|
||||
is(rc, ot::st::badly_encoded, "conversion from bad UTF-8 fails");
|
||||
}
|
||||
|
||||
void check_shell_esape()
|
||||
{
|
||||
is(ot::shell_escape("foo"), "'foo'", "simple string");
|
||||
is(ot::shell_escape("a'b"), "'a'\\''b'", "string with a simple quote");
|
||||
is(ot::shell_escape("a!b"), "'a'\\!'b'", "string with a bang");
|
||||
is(ot::shell_escape("a!b'c!d'e"), "'a'\\!'b'\\''c'\\!'d'\\''e'", "string with a bang");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
plan(2);
|
||||
plan(3);
|
||||
run(check_partial_files, "test partial files");
|
||||
run(check_converter, "test encoding converter");
|
||||
run(check_shell_esape, "test shell escaping");
|
||||
return 0;
|
||||
}
|
||||
|
Reference in New Issue
Block a user