4
\$\begingroup\$
#!/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:

  1. Could the implementation have been shorter?
  2. Are there any failing edgecases or bugs in my script?
  3. What do you think about the behaviour choices I made?
asked Oct 3, 2024 at 20:27
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

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:

  1. Is this race-free in the sense of thwarting an attacker?
  2. 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
answered Oct 3, 2024 at 21:42
\$\endgroup\$
4
  • \$\begingroup\$ Thanks! I agree my implementation is probably much longer than it needs to be, do you know how I could shorten it? \$\endgroup\$ Commented 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 with import os. \$\endgroup\$ Commented 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\$ Commented 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\$ Commented Oct 6, 2024 at 16:32

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.