mirror of
https://github.com/fmang/opustags.git
synced 2024-11-13 00:42:46 +01:00
write the output to a temporary file
This commit is contained in:
parent
a74ea34352
commit
1d6ca8fc59
23
opustags.1
23
opustags.1
@ -15,7 +15,7 @@ opustags \- Opus comment editor
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
\fBopustags\fP can read and edit the comment header of an Opus file.
|
||||
It basically has two modes: read-only, and read-write for tag edition.
|
||||
It basically has two modes: read-only, and read-write for tag editing.
|
||||
.PP
|
||||
In read-only mode, only the beginning of \fIINPUT\fP is read, and the tags are
|
||||
printed on standard output.
|
||||
@ -23,10 +23,12 @@ printed on standard output.
|
||||
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 edition mode, you need to specify an output file (or \fB-\fP for standard output). It must be
|
||||
different from the input file. To overwrite the input file, use \fB--in-place\fP.
|
||||
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
|
||||
temporary file and then moved to its final location on success. On error, the temporary output file
|
||||
is deleted.
|
||||
.PP
|
||||
Tag edition can be performed with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
|
||||
Tag editing can be performed with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
|
||||
options. Options can be specified in any order and don’t conflict with each other.
|
||||
First the specified tags are deleted, then the new tags are added.
|
||||
.PP
|
||||
@ -52,16 +54,15 @@ The input file will be read, its tags edited, then written to the specified outp
|
||||
\fIFILE\fP is \fB-\fP then the resulting Opus file will be written to standard output.
|
||||
The output file can’t be the same as the input file.
|
||||
.TP
|
||||
.B \-i, \-\-in-place\fR[=\fP\fISUFFIX\fP\fR]\fP
|
||||
Use this when you want to modify the input file in place. opustags will create a temporary output
|
||||
file with the specified suffix (.otmp by default), and move it to the location of the input file on
|
||||
success. If a file with the same name as the temporary file already exists, it will be overwritten
|
||||
without warning.
|
||||
.B \-i, \-\-in-place
|
||||
Overwrite the input file instead of creating a separate output file. It has the same effect as
|
||||
setting \fB--output\fP to the same path as the input file and enabling \fB--overwrite\fP.
|
||||
This option conflicts with \fB--output\fP.
|
||||
.TP
|
||||
.B \-y, \-\-overwrite
|
||||
By default, \fBopustags\fP refuses to overwrite an already existent file. Use
|
||||
this option to allow that.
|
||||
By default, \fBopustags\fP refuses to overwrite an already-existent file.
|
||||
Use \fB-y\fP to allow overwriting.
|
||||
Note that this option is not needed when the output is a special file like \fI/dev/null\fP.
|
||||
.TP
|
||||
.B \-d, \-\-delete \fIFIELD\fP
|
||||
Delete all the tags whose field name is \fIFIELD\fP. They may be several one of them, though usually
|
||||
|
148
src/cli.cc
148
src/cli.cc
@ -59,9 +59,10 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
|
||||
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:i::yd:a:s:DS", getopt_options, NULL)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DS", getopt_options, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'h':
|
||||
opt.print_help = true;
|
||||
@ -74,11 +75,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
|
||||
return {st::bad_arguments, "Output file path cannot be empty."};
|
||||
break;
|
||||
case 'i':
|
||||
if (opt.in_place != nullptr)
|
||||
return {st::bad_arguments, "Cannot specify --in-place more than once."};
|
||||
opt.in_place = optarg == nullptr ? ".otmp" : optarg;
|
||||
if (strcmp(opt.in_place, "") == 0)
|
||||
return {st::bad_arguments, "In-place suffix cannot be empty."};
|
||||
in_place = true;
|
||||
break;
|
||||
case 'y':
|
||||
opt.overwrite = true;
|
||||
@ -117,13 +114,17 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
|
||||
opt.path_in = argv[optind];
|
||||
if (opt.path_in.empty())
|
||||
return {st::bad_arguments, "Input file path cannot be empty."};
|
||||
if (opt.in_place != nullptr && !opt.path_out.empty())
|
||||
return {st::bad_arguments, "Cannot combine --in-place and --output."};
|
||||
if (in_place) {
|
||||
if (!opt.path_out.empty())
|
||||
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;
|
||||
}
|
||||
if (opt.path_in == "-" && opt.set_all)
|
||||
return {st::bad_arguments,
|
||||
"Cannot use standard input as input file when --set-all is specified."};
|
||||
if (opt.path_in == "-" && opt.in_place)
|
||||
return {st::bad_arguments, "Cannot modify standard input in place."};
|
||||
return st::ok;
|
||||
}
|
||||
|
||||
@ -256,73 +257,76 @@ ot::status ot::run(const ot::options& opt)
|
||||
return st::ok;
|
||||
}
|
||||
|
||||
std::string path_out = opt.in_place ? opt.path_in + opt.in_place : opt.path_out;
|
||||
if (!path_out.empty() && path_out != "-") {
|
||||
struct stat input_info;
|
||||
if (opt.path_in != "-" && stat(opt.path_in.c_str(), &input_info) == -1)
|
||||
return {ot::st::fatal_error,
|
||||
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
|
||||
struct stat output_info;
|
||||
if (stat(opt.path_out.c_str(), &output_info) == 0) {
|
||||
if (opt.path_in != "-" &&
|
||||
input_info.st_dev == output_info.st_dev &&
|
||||
input_info.st_ino == output_info.st_ino)
|
||||
return {ot::st::fatal_error,
|
||||
"Input and output cannot be the same file. "
|
||||
"Use --in-place instead."};
|
||||
if (!opt.overwrite)
|
||||
return {ot::st::fatal_error,
|
||||
"'" + path_out + "' already exists. Use -y to overwrite."};
|
||||
} else if (errno != ENOENT) {
|
||||
return {ot::st::fatal_error,
|
||||
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
|
||||
}
|
||||
}
|
||||
|
||||
ot::file input;
|
||||
if (opt.path_in == "-") {
|
||||
if (opt.path_in == "-")
|
||||
input = stdin;
|
||||
else if ((input = fopen(opt.path_in.c_str(), "r")) == nullptr)
|
||||
return {ot::st::standard_error,
|
||||
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
|
||||
ot::ogg_reader reader(input.get());
|
||||
|
||||
/* Read-only mode. */
|
||||
if (opt.path_out.empty())
|
||||
return process(reader, nullptr, opt);
|
||||
|
||||
/* Read-write mode.
|
||||
*
|
||||
* The output pointer is set to one of:
|
||||
* - stdout for "-",
|
||||
* - final_output.get() for special files like /dev/null,
|
||||
* - temporary_output.get() for regular files.
|
||||
*
|
||||
* We use a temporary output file for the following reasons:
|
||||
* 1. The partial .opus output may be seen by softwares like media players, or through
|
||||
* inotify for the most attentive process.
|
||||
* 2. If the process crashes badly, or the power cuts off, we don't want to leave a partial
|
||||
* file at the final location. The temporary file is still going to stay but will have an
|
||||
* obvious name.
|
||||
* 3. If we're overwriting a regular file, we'd rather avoid wiping its content before we
|
||||
* even started reading the input file. That way, the original file is always preserved
|
||||
* on error or crash.
|
||||
* 4. It is necessary for in-place editing. We can't reliably open the same file as both
|
||||
* input and output.
|
||||
*/
|
||||
|
||||
FILE* output = nullptr;
|
||||
ot::partial_file temporary_output;
|
||||
ot::file final_output;
|
||||
|
||||
ot::status rc = ot::st::ok;
|
||||
struct stat output_info;
|
||||
if (opt.path_out == "-") {
|
||||
output = stdout;
|
||||
} else if (stat(opt.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)
|
||||
rc = {ot::st::standard_error,
|
||||
"Could not open '" + opt.path_out + "' for writing: " +
|
||||
strerror(errno)};
|
||||
output = final_output.get();
|
||||
} else if (opt.overwrite) {
|
||||
rc = temporary_output.open(opt.path_out.c_str());
|
||||
output = temporary_output.get();
|
||||
} else {
|
||||
rc = {ot::st::fatal_error,
|
||||
"'" + opt.path_out + "' already exists. Use -y to overwrite."};
|
||||
}
|
||||
} else if (errno == ENOENT) {
|
||||
rc = temporary_output.open(opt.path_out.c_str());
|
||||
output = temporary_output.get();
|
||||
} else {
|
||||
input = fopen(opt.path_in.c_str(), "r");
|
||||
if (input == nullptr)
|
||||
return {ot::st::standard_error,
|
||||
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
|
||||
rc = {ot::st::fatal_error,
|
||||
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
|
||||
}
|
||||
|
||||
ot::file output;
|
||||
if (path_out == "-") {
|
||||
output.reset(stdout);
|
||||
} else if (!path_out.empty()) {
|
||||
output = fopen(path_out.c_str(), "w");
|
||||
if (output == nullptr)
|
||||
return {ot::st::standard_error,
|
||||
"Could not open '" + path_out + "' for writing: " + strerror(errno)};
|
||||
}
|
||||
|
||||
ot::status rc;
|
||||
{
|
||||
ot::ogg_reader reader(input.get());
|
||||
std::unique_ptr<ot::ogg_writer> writer;
|
||||
if (output != nullptr)
|
||||
writer = std::make_unique<ot::ogg_writer>(output.get());
|
||||
rc = process(reader, writer.get(), opt);
|
||||
/* delete reader and writer before closing the files */
|
||||
}
|
||||
|
||||
input.reset();
|
||||
output.reset();
|
||||
|
||||
if (rc != ot::st::ok) {
|
||||
if (!path_out.empty() && path_out != "-")
|
||||
remove(path_out.c_str());
|
||||
if (rc != ot::st::ok)
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (opt.in_place) {
|
||||
if (rename(path_out.c_str(), opt.path_in.c_str()) == -1)
|
||||
return {ot::st::fatal_error,
|
||||
"Could not move the result to '" + opt.path_in + "': " + strerror(errno)};
|
||||
}
|
||||
ot::ogg_writer writer(output);
|
||||
rc = process(reader, &writer, opt);
|
||||
if (rc == ot::st::ok)
|
||||
rc = temporary_output.commit();
|
||||
|
||||
return ot::st::ok;
|
||||
return rc;
|
||||
}
|
||||
|
@ -386,8 +386,7 @@ void delete_comments(opus_tags& tags, const char* field_name);
|
||||
*/
|
||||
|
||||
/**
|
||||
* Structured representation of the command-line arguments to opustags. It must faithfully represent
|
||||
* what the user asked for, with as little interpretation or deduction as possible.
|
||||
* Structured representation of the command-line arguments to opustags.
|
||||
*/
|
||||
struct options {
|
||||
/**
|
||||
@ -404,22 +403,16 @@ struct options {
|
||||
std::string path_in;
|
||||
/**
|
||||
* Path to the optional file. The special "-" string means stdout. When empty, opustags runs
|
||||
* in read-only mode.
|
||||
* in read-only mode. For in-place editing, path_out is defined equal to path_in.
|
||||
*
|
||||
* Option: --output
|
||||
* Options: --output, --in-place
|
||||
*/
|
||||
std::string path_out;
|
||||
/**
|
||||
* If null, in-place editing is disabled. Otherwise, it points to the suffix to add to the
|
||||
* file name.
|
||||
* By default, opustags won't overwrite the output file if it already exists. This can be
|
||||
* forced with --overwrite. It is also enabled by --in-place.
|
||||
*
|
||||
* Option: --in-place
|
||||
*/
|
||||
const char* in_place = nullptr;
|
||||
/**
|
||||
* By default, opustags won't overwrite the output file if it already exists.
|
||||
*
|
||||
* Option: --overwrite
|
||||
* Options: --overwrite, --in-place
|
||||
*/
|
||||
bool overwrite = false;
|
||||
/**
|
||||
|
11
t/cli.cc
11
t/cli.cc
@ -34,15 +34,14 @@ void check_good_arguments()
|
||||
throw failure("did not catch --help");
|
||||
|
||||
opt = parse({"opustags", "x", "--output", "y", "-D", "-s", "X=Y Z"});
|
||||
if (opt.in_place != nullptr || opt.path_in != "x" || opt.path_out != "y" || !opt.delete_all ||
|
||||
if (opt.path_in != "x" || opt.path_out != "y" || !opt.delete_all || opt.overwrite ||
|
||||
opt.to_delete.size() != 1 || opt.to_delete[0] != "X=Y Z" ||
|
||||
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", "-y", "x", "-S", "-a", "x=y z", "-i"});
|
||||
if (opt.in_place == nullptr || opt.path_in != "x" || !opt.path_out.empty() ||
|
||||
!opt.set_all || !opt.overwrite || opt.to_delete.size() != 0 ||
|
||||
opt.to_add.size() != 1 || opt.to_add[0] != "x=y z")
|
||||
opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
|
||||
if (opt.path_in != "x" || opt.path_out != "x" || !opt.set_all || !opt.overwrite ||
|
||||
opt.to_delete.size() != 0 || opt.to_add.size() != 1 || opt.to_add[0] != "x=y z")
|
||||
throw failure("unexpected option parsing result for case #2");
|
||||
}
|
||||
|
||||
@ -58,7 +57,6 @@ void check_bad_arguments()
|
||||
};
|
||||
error_case({"opustags"}, "No arguments specified. Use -h for help.", "no arguments");
|
||||
error_case({"opustags", "--output", ""}, "Output file path cannot be empty.", "empty output path");
|
||||
error_case({"opustags", "--in-place="}, "In-place suffix cannot be empty.", "empty in-place suffix");
|
||||
error_case({"opustags", "--delete", "X="}, "Invalid field name 'X='.", "bad field name for -d");
|
||||
error_case({"opustags", "-a", "X"}, "Invalid comment 'X'.", "bad comment for -a");
|
||||
error_case({"opustags", "--set", "X"}, "Invalid comment 'X'.", "bad comment for --set");
|
||||
@ -76,7 +74,6 @@ void check_bad_arguments()
|
||||
error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place");
|
||||
error_case({"opustags", "-o", "x", "--output", "y", "z"},
|
||||
"Cannot specify --output more than once.", "double output");
|
||||
error_case({"opustags", "-i", "-i", "z"}, "Cannot specify --in-place more than once.", "double in-place");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
@ -100,9 +100,7 @@ error: 'out.opus' already exists. Use -y to overwrite.
|
||||
EOF
|
||||
is(md5('out.opus'), 'd41d8cd98f00b204e9800998ecf8427e', 'the output wasn\'t written');
|
||||
|
||||
is_deeply(opustags(qw(out.opus -o out.opus)), ['', <<'EOF', 256], 'output and input can\'t be the same');
|
||||
error: Input and output cannot be the same file. Use --in-place instead.
|
||||
EOF
|
||||
is_deeply(opustags(qw(gobble.opus -o /dev/null)), ['', '', 0], 'write to /dev/null');
|
||||
|
||||
is_deeply(opustags(qw(gobble.opus -o out.opus --overwrite)), ['', '', 0], 'overwrite');
|
||||
is(md5('out.opus'), '111a483596ac32352fbce4d14d16abd2', 'successfully overwritten');
|
||||
|
Loading…
Reference in New Issue
Block a user