mirror of
https://github.com/fmang/opustags.git
synced 2025-01-15 12:43:17 +01:00
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:
parent
73a54d7ab7
commit
84e238a4a9
@ -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
|
||||
|
@ -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
|
||||
|
81
src/cli.cc
81
src/cli.cc
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
17
t/cli.cc
17
t/cli.cc
@ -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");
|
||||
|
23
t/opustags.t
23
t/opustags.t
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user