Add support for multiple input files with --in-place

Co-authored-by: Frédéric Mangano-Tarumi <fmang@mg0.fr>
This commit is contained in:
Reuben Thomas 2020-09-26 12:12:15 +01:00 committed by GitHub
parent 73a54d7ab7
commit 84e238a4a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 101 additions and 58 deletions

View File

@ -48,12 +48,13 @@ Documentation
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS -i FILE...
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-o, --output FILE specify the output file
-i, --in-place overwrite the input file
-i, --in-place overwrite the input files
-y, --overwrite overwrite the output file if it already exists
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD[=VALUE] delete previously existing comments

View File

@ -10,6 +10,11 @@ opustags \- Ogg Opus tag editor
.br
.B opustags
.I OPTIONS
.B -i
.R \fIFILE\fP...
.br
.B opustags
.I OPTIONS
.B -o
.I OUTPUT INPUT
.SH DESCRIPTION
@ -24,7 +29,7 @@ You can use the options below to edit the tags before printing them.
This could be useful to preview some changes before writing them.
.PP
In editing mode, you need to specify an output file with \fB--output\fP, or use \fB--in-place\fP to
overwrite the input file. If the output is a regular file, the result is first written to a
overwrite the input files. If the output is a regular file, the result is first written to a
temporary file and then moved to its final location on success. On error, the temporary output file
is deleted.
.PP

View File

@ -23,12 +23,13 @@ R"raw(
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS -i FILE...
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-o, --output FILE specify the output file
-i, --in-place overwrite the input file
-i, --in-place overwrite the input files
-y, --overwrite overwrite the output file if it already exists
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD[=VALUE] delete previously existing comments
@ -62,7 +63,6 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
opt = {};
if (argc == 1)
return {st::bad_arguments, "No arguments specified. Use -h for help."};
bool in_place = false;
int c;
optind = 0;
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DS", getopt_options, NULL)) != -1) {
@ -76,7 +76,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
opt.path_out = optarg;
break;
case 'i':
in_place = true;
opt.in_place = true;
break;
case 'y':
opt.overwrite = true;
@ -115,22 +115,23 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm
}
if (opt.print_help)
return st::ok;
if (optind != argc - 1)
return {st::bad_arguments, "Exactly one input file must be specified."};
opt.path_in = argv[optind];
if (opt.path_in.empty())
return {st::bad_arguments, "Input file path cannot be empty."};
if (in_place) {
if (opt.in_place) {
if (opt.path_out)
return {st::bad_arguments, "Cannot combine --in-place and --output."};
if (opt.path_in == "-")
return {st::bad_arguments, "Cannot modify standard input in place."};
opt.path_out = opt.path_in;
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]);
}
if (opt.path_in == "-" && set_all)
return {st::bad_arguments,
"Cannot use standard input as input file when --set-all is specified."};
if (set_all) {
// Read comments from stdin and prepend them to opt.to_add.
std::vector<std::string> comments;
@ -319,23 +320,18 @@ static ot::status process(ot::ogg_reader& reader, ot::ogg_writer* writer, const
return ot::st::ok;
}
ot::status ot::run(const ot::options& opt)
static ot::status run_single(const ot::options& opt, const std::string& path_in, const std::optional<std::string>& path_out)
{
if (opt.print_help) {
fputs(help_message, stdout);
return st::ok;
}
ot::file input;
if (opt.path_in == "-")
if (path_in == "-")
input = stdin;
else if ((input = fopen(opt.path_in.c_str(), "r")) == nullptr)
else if ((input = fopen(path_in.c_str(), "r")) == nullptr)
return {ot::st::standard_error,
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
"Could not open '" + path_in + "' for reading: " + strerror(errno)};
ot::ogg_reader reader(input.get());
/* Read-only mode. */
if (!opt.path_out)
if (!path_out)
return process(reader, nullptr, opt);
/* Read-write mode.
@ -363,30 +359,30 @@ ot::status ot::run(const ot::options& opt)
ot::status rc = ot::st::ok;
struct stat output_info;
if (opt.path_out == "-") {
if (path_out == "-") {
output = stdout;
} else if (stat(opt.path_out->c_str(), &output_info) == 0) {
} else if (stat(path_out->c_str(), &output_info) == 0) {
/* The output file exists. */
if (!S_ISREG(output_info.st_mode)) {
/* Special files are opened for writing directly. */
if ((final_output = fopen(opt.path_out->c_str(), "w")) == nullptr)
if ((final_output = fopen(path_out->c_str(), "w")) == nullptr)
rc = {ot::st::standard_error,
"Could not open '" + opt.path_out.value() + "' for writing: " +
"Could not open '" + path_out.value() + "' for writing: " +
strerror(errno)};
output = final_output.get();
} else if (opt.overwrite) {
rc = temporary_output.open(opt.path_out->c_str());
rc = temporary_output.open(path_out->c_str());
output = temporary_output.get();
} else {
rc = {ot::st::error,
"'" + opt.path_out.value() + "' already exists. Use -y to overwrite."};
"'" + path_out.value() + "' already exists. Use -y to overwrite."};
}
} else if (errno == ENOENT) {
rc = temporary_output.open(opt.path_out->c_str());
rc = temporary_output.open(path_out->c_str());
output = temporary_output.get();
} else {
rc = {ot::st::error,
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
"Could not identify '" + path_in + "': " + strerror(errno)};
}
if (rc != ot::st::ok)
return rc;
@ -398,3 +394,22 @@ ot::status ot::run(const ot::options& opt)
return rc;
}
ot::status ot::run(const ot::options& opt)
{
if (opt.print_help) {
fputs(help_message, stdout);
return st::ok;
}
ot::status global_rc = st::ok;
for (const auto& path_in : opt.paths_in) {
ot::status rc = run_single(opt, path_in, opt.in_place ? path_in : opt.path_out);
if (rc != st::ok) {
global_rc = st::error;
if (!rc.message.empty())
fprintf(stderr, "%s: error: %s\n", path_in.c_str(), rc.message.c_str());
}
}
return global_rc;
}

View File

@ -20,12 +20,8 @@ int main(int argc, char** argv) {
ot::status rc = ot::parse_options(argc, argv, opt, stdin);
if (rc == ot::st::ok)
rc = ot::run(opt);
else if (!rc.message.empty())
fprintf(stderr, "error: %s\n", rc.message.c_str());
if (rc != ot::st::ok) {
if (!rc.message.empty())
fprintf(stderr, "error: %s\n", rc.message.c_str());
return EXIT_FAILURE;
} else {
return EXIT_SUCCESS;
}
return rc == ot::st::ok ? EXIT_SUCCESS : EXIT_FAILURE;
}

View File

@ -374,14 +374,16 @@ struct options {
*/
bool print_help = false;
/**
* Path to the input file. It cannot be empty. The special "-" string means stdin.
* Paths to the input files. The special string "-" means stdin.
*
* This is the mandatory non-flagged parameter.
* At least one input file must be given. If `--in-place` is used,
* more than one may be given.
*/
std::string path_in;
std::vector<std::string> paths_in;
/**
* Optional path to output file. The special "-" string means stdout. When absent, opustags
* runs in read-only mode. For in-place editing, path_out is defined equal to path_in.
* Optional path to output file. The special string "-" means stdout. For in-place
* editing, the input file name is used. If no output file name is supplied, and
* --in-place is not used, opustags runs in read-only mode.
*
* Options: --output, --in-place
*/
@ -393,6 +395,12 @@ struct options {
* Options: --overwrite, --in-place
*/
bool overwrite = false;
/**
* Process files in-place.
*
* Options: --in-place
*/
bool in_place = false;
/**
* List of comments to delete. Each string is a selector according to the definition of
* #delete_comments.

View File

@ -69,16 +69,22 @@ void check_good_arguments()
throw failure("did not catch --help");
opt = parse({"opustags", "x", "--output", "y", "-D", "-s", "X=Y Z", "-d", "a=b"});
if (opt.path_in != "x" || 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" ||
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")
throw failure("unexpected option parsing result for case #1");
opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
if (opt.path_in != "x" || opt.path_out != "x" || !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")
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")
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");
}
void check_bad_arguments()
@ -106,7 +112,6 @@ void check_bad_arguments()
error_case({"opustags", "-x=y"}, "Unrecognized option '-x'.", "unrecognized short option with value");
error_case({"opustags", "--derp=y"}, "Unrecognized option '--derp=y'.", "unrecognized long option with value");
error_case({"opustags", "-aX=Y"}, "Exactly one input file must be specified.", "no input file");
error_case({"opustags", ""}, "Input file path cannot be empty.", "empty input file path");
error_case({"opustags", "-i", "-o", "/dev/null", "-"}, "Cannot combine --in-place and --output.", "in-place + output");
error_case({"opustags", "-S", "-"}, "Cannot use standard input as input file when --set-all is specified.",
"set all and read opus from stdin");

View File

@ -4,10 +4,11 @@ use strict;
use warnings;
use utf8;
use Test::More tests => 37;
use Test::More tests => 41;
use Digest::MD5;
use File::Basename;
use File::Copy;
use IPC::Open3;
use List::MoreUtils qw(any);
use Symbol 'gensym';
@ -57,12 +58,13 @@ $version
Usage: opustags --help
opustags [OPTIONS] FILE
opustags OPTIONS -i FILE...
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-o, --output FILE specify the output file
-i, --in-place overwrite the input file
-i, --in-place overwrite the input files
-y, --overwrite overwrite the output file if it already exists
-a, --add FIELD=VALUE add a comment
-d, --delete FIELD[=VALUE] delete previously existing comments
@ -81,7 +83,7 @@ error: Unrecognized option '--derp'.
EOF
is_deeply(opustags('../opustags'), ['', <<"EOF", 256], 'not an Ogg stream');
error: Input is not a valid Ogg file.
../opustags: error: Input is not a valid Ogg file.
EOF
####################################################################################################
@ -110,7 +112,7 @@ umask($previous_umask);
# empty out.opus
{ my $fh; open($fh, '>', 'out.opus') and close($fh) or die }
is_deeply(opustags(qw(gobble.opus -o out.opus)), ['', <<'EOF', 256], 'refuse to override');
error: 'out.opus' already exists. Use -y to overwrite.
gobble.opus: error: 'out.opus' already exists. Use -y to overwrite.
EOF
is(md5('out.opus'), 'd41d8cd98f00b204e9800998ecf8427e', 'the output wasn\'t written');
@ -208,6 +210,17 @@ is_deeply(opustags('-', '-o', '-', {in => $data, mode => ':raw'}), [$data, '', 0
unlink('out.opus');
# Test --in-place
unlink('out2.opus');
copy('gobble.opus', 'out.opus');
is_deeply(opustags(qw(out.opus --add BAR=baz -o out2.opus)), ['', '', 0], 'process multiple files with --in-place');
is_deeply(opustags(qw(--in-place --add FOO=bar out.opus out2.opus)), ['', '', 0], 'process multiple files with --in-place');
is(md5('out.opus'), '30ba30c4f236c09429473f36f8f861d2', 'the tags were added correctly (out.opus)');
is(md5('out2.opus'), '0a4d20c287b2e46b26cb0eee353c2069', 'the tags were added correctly (out2.opus)');
unlink('out.opus');
unlink('out2.opus');
####################################################################################################
# Test muxed streams
@ -215,7 +228,7 @@ system('ffmpeg -loglevel error -y -i gobble.opus -c copy -map 0:0 -map 0:0 -shor
or BAIL_OUT('could not create a muxed stream');
is_deeply(opustags('muxed.ogg'), ['', <<'END_ERR', 256], 'muxed streams detection');
error: Muxed streams are not supported yet.
muxed.ogg: error: Muxed streams are not supported yet.
END_ERR
unlink('muxed.ogg');