#!/bin/zsh
function mvbk {
alias no_overwrite_mv="/usr/bin/yes n | /bin/mv -i --"
destination="${@[-1]}"
for file in "${@[1,-2]}"
do
error="$(no_overwrite_mv "$file" "$destination" 2>&1 >/dev/null)"
if [[ "$error" == "overwrite "*"? (y/n [n]) not overwritten" ]]
then
existing="${error#"overwrite "}"
existing="${existing%"? (y/n [n]) not overwritten"}"
count=1
while [[ "$(no_overwrite_mv "$existing" "${existing:r}-$count.${existing:e}" 2>&1 >/dev/null)" \
== "overwrite "*"? (y/n [n]) not overwritten" ]]
do
((count++))
done
/bin/mv "$file" "$destination"
else
echo $error >/dev/stderr
fi
done
}
mvbk "$@"
This is a zsh script for macOS.
It approximates mv --backup=numbered
, at least capturing what I think is useful about that command; being able to rename existing files so that they won't get replaced by the moved file, without obscuring much information about them including their filename.
I like mine even more than the GNU --backup=numbered
option, because it doesn't change the file extension. This is useful so that the old file can still be used by the same programs without doing extra work in between.
mvbk source target
mvbk source ... directory
mvbk file1.ext file2.ext
- if file2.ext
exists, it is renamed file2-1.ext
. If file2-1.ext
already exists, it is renamed file2-2.ext
, etc. Then finally file1.ext
is renamed file2.ext
.
I'm most interested in:
- Could the implementation have been shorter?
- Are there any failing edgecases or bugs in my script?
- What do you think about the behaviour choices I made?
1 Answer 1
DRY
alias no_overwrite_mv="/usr/bin/yes n | /bin/mv -i --"
Good job on not repeating yourself, thank you! This is used at two calling sites further on. Looks great, nice contract.
The implementation seems a bit, meh? On the heavyweight side?
There's two concerns we might have:
- Is this race-free in the sense of thwarting an attacker?
- Does this avoid stomping on a file that has been there "for a while"?
Item (1.) tends to boil down to easier to ask forgiveness than permission (committing to a proposed action) instead of look before you leap. Often it involves operating on an open file descriptor, something I do not yet see how we could apply here. I believe this isn't really a serious concern in your (non-adversarial) use case.
Item (2.) is easier to tackle, and you are pushing the
"does a file already exist?" test down into the mv
program.
I feel it would be more natural to test -e "$FILE"
, or similar,
up in the OP code.
Supplying an interactive response of "no" seems a bit kludgey;
it's a bolt-on after the fact.
If the usual bash verbs don't quite give you enough access to chapter 2 filesystem operations like rename(), then consider relying on a Perl or Python script to accomplish that.
serial numbers
The original Specification and its Implementation are great, they achieve their design goals. Here is a minor feature request.
Producing a sequence of README-1, README-2, ..., can certainly work well.
But the resulting serials are not super informative to humans,
plus the lexical sorting of README-9 vs README-10 is unhelpful.
Consider asking stat()
what the mtime is, and then trying a small sequence of candidate destination names,
iterating until finding an unused one:
- README-%y-%m-%d
- README-%y-%m-%d_%H-%M
- README-%y-%m-%d_%H-%M-%S
- README-%y-%m-%d_%H-%M-%S-serial
-
\$\begingroup\$ Thanks! I agree my implementation is probably much longer than it needs to be, do you know how I could shorten it? \$\endgroup\$user98809– user988092024年10月04日 07:32:53 +00:00Commented Oct 4, 2024 at 7:32
-
\$\begingroup\$ I suggested using
test -e
to see if file exists, or coding it in a more suitable language which gives direct access to chapter(2) primitives, such as python withimport os
. \$\endgroup\$J_H– J_H2024年10月04日 16:14:28 +00:00Commented Oct 4, 2024 at 16:14 -
\$\begingroup\$ I don't think using
test -e
would reduce the number of lines in the code that much, I'd still have the same number and order of loops and logical branches right? \$\endgroup\$user98809– user988092024年10月06日 14:10:55 +00:00Commented Oct 6, 2024 at 14:10 -
\$\begingroup\$ Making it easier to read (and maintain) a more straightforward implementation is the primary motivation. Say what you mean, and mean what you say, rather than going through extra layers unrelated to the business problem. The whole business of interactive prompts / responses appearing in the middle of a batch job is just a distraction. \$\endgroup\$J_H– J_H2024年10月06日 16:32:17 +00:00Commented Oct 6, 2024 at 16:32