17

I've just lost a small part of my audio collection, by a stupid mistake I made. :-(
GLADLY I had a fairly recent backup, but it was still irritating. Apart from yours truly, the other culprit doing the mischief was mv, which will show as follows:

The audio files had a certain scheme:

ARTIST - Some Title YY.mp3

where YY is the 2-digit year specification.

mkdir 90<invisible control character>

(Up to this moment, I did not know that I had actually typed one third excess character which was invisible ...!)
Instead of having all in one directory, I wanted to have all 1990s music in one directory. So I typed:

find . -name '* 9?.mp3' -exec mv {} 90 \;

Not so hard to get the idea what happened eh? :->
The (disastrous) result was a virgin empty directory called '90 something' (with something being the "invisible" control character) and one single file called '90', overwritten n times.

ALL FILES WERE GONE. :-(( (obviously)

Wish mv would've checked in time whether the signature of the destination "file" (remember on *NIX: Everything Is A File) starts with a d------ (e. g. drwxr-xr-x). And, of course, whether the destination exists at all. There is a variant of the aforementioned scenario, when you simply forgot to mkdir the directory first. (but of course, you assumed that it's there...)

Even our pet-hate OS starting with the capital W DOES DO THIS. You get even prompted to specify the type of destination (file? directory?) if you ask for it.

Hence, I'm wondering if we *NIXers still have to write ourselves a "mv scriptlet" just to avoid these kinds of most unwanted surprises.

syntaxerror
  • 2,246
  • 2
    Not all files were gone. At least one .mp3 should be there with the name 90, it could have been one for which you did not have a backup. – Anthon Nov 21 '14 at 08:38
  • 2
    Heh, you've got a cynical sense of humor, you nut you! :-P Well, that was the file called the "one single file" in bold print in my OP. :) – syntaxerror Nov 21 '14 at 08:45
  • 2
    mv isn't the problem here, technically, it doesn't know that you are moving a series of files. You are running mv one time for each file. That's how find -exec ; works. If you had used find -exec + (as in some of the comments) mv would have screamed as soon as it got more than one argument. – Etan Reisner Nov 23 '14 at 12:49
  • Though running mv for each single file might seem a bit less thought-out at first, it will (as I had said previously) be the only sane solution once source files are scattered among various subdirectories. That in my test-case, source files were all in one directory does not mean that it's my actual test case. It's in fact just a simplification, because I may easily elaborate on that on my own later. Plus, it makes questions less time-consuming to read due to their reduced length. :) – syntaxerror Nov 23 '14 at 13:03
  • Why would you expect mv to require that the destination exists? mv oldfile newfile is the way to rename a file, and it's silly to expect newfile to exist already and be a directory. – Barmar Nov 26 '14 at 19:29
  • I might have avoided the pitfall, because, when I type a command, I usually autocomplete (or check) existing file/directory names with a tab, even when they are very short, just to make sure. – Walter Tross Nov 27 '14 at 22:38

6 Answers6

37

You can append a / to the destination if you want to move files to a directory. In case the directory does not exist you'll receive an error:

mv somefile somedir/
mv: cannot move ‘somefile’ to ‘somedir/’: Not a directory

In case the directory exists, it moves the file into that directory.

Marco
  • 33,548
  • 4
    Thank you so much! That should be my future life-saver then. I owe you one. (Just as a note, a friend of mine had made the very same mistake some years ago, so I feel I'm not alone.) – syntaxerror Nov 21 '14 at 08:43
  • 1
    Also, when using certain Shells, you have the Tab option to auto-complete filenames for you. If I can't quite remember the directory name, and don't want a mess like you recently had, just punch in a character or two of the directory name and HIT TAB. Then you can be Sure, that it exists, because auto-complete put it there.... – Andyz Smith Nov 23 '14 at 13:47
  • @AndyzSmith Well, that's the very thing. You may call it a habit of mine to only ever use TAB for complicated directories or paths, but not for 2-letter-type ones.:) But come to think of it...perhaps I should really consider the latter case as well from now on. – syntaxerror Dec 12 '14 at 05:20
20

The GNU coreutils mv already has an option specifying that you want to move to a directory: -t / --target-directory. If the argument to that option doesn't exists, mv will complain instead of moving all of your files to the same filename.

I would have written your mover as follows:

find . -name '* 9?.mp3' -exec mv -t 90 {} +

Note the use of + instead of \;, globbing as many filenames together as possible, resulting in faster execution.

Anthon
  • 79,293
  • Thanks. (Hoping it's not one of those GNU-isms again, though.) – syntaxerror Nov 21 '14 at 08:41
  • 2
    @syntaxerror. It is a GNUism. POSIXly: find . -name '* 9?.mp3' -exec sh -c 'exec mv "$@" 90/' sh {} + – Stéphane Chazelas Nov 21 '14 at 15:39
  • Thank you very much for this most SNEAKY one-liner! Just think it were a +5 I gave you. :) Will make lots of trial-and-error attempts unnecessary.--- And you know too well why I made that remark. Just need to be on a machine that is straightly POSIX (doesn't happen too rarely), and I'm going to have the next problem right there. :) – syntaxerror Nov 21 '14 at 15:50
  • 1
    @syntaxerror If this answer solves your issue please take a minute and click the check mark under the vote count to the left, this will signify to everyone that your issue's been resolved and is the way that thanks are expressed in the site. I saw that only a few of your other answered questions have accepted answers, you might want to reveiw those too. – Anthon Nov 21 '14 at 16:49
  • No, it's simply purpose. I'd frequently wait several weeks or sometimes 2 months until I accept an answer. The reason is that some very knowledgeable people have a VERY loaded schedule and might only find time to give their (usually best) answer after a couple weeks. So I always find it just respectful to wait for them to stop by. Well, and if they really don't, I won't hesitate to hit the checkmark, for sure. Besides, I don't see why some people are always in a hurry so much on SE + its flavors. Easy does it, fellas. Don't jump the gun. :) This ain't your boss pressing on you. – syntaxerror Nov 21 '14 at 17:38
  • You can't be serious. If I was in admin staff here, this would never be possible! Remember this gives a decent rep boost with the answering user when you accept their answer! Wouldn't this rep be taken away from the user whose answer you accepted previously? That would mean giving rep boost - user is happy - and snatch it away later, so that user may get really annoyed or upset? Awwww. I couldn't believe it. :-O – syntaxerror Nov 23 '14 at 20:45
10

Additionally, if you generally plan to avoid accidental overwrites in the future, there is the -i option for mv. I personally cannot think of any drawbacks if you

alias mv='mv -i'

If you then need to overwrite something, simply pass the -f option.

Aliases only take effect if you type the command directly into an interactive shell, not for cases like invocation by find. You could have run

find . -name '* 9?.mp3' -exec mv -i {} 90 \;

and then you would have been prompted if mv had tried to overwrite an existing file.

ayekat
  • 334
  • 4
    Or - as you probably did not know - type ´ \mv instead ofmv`. This less known "trick" will cause the command with the backslash prepended to it to ignore any alias definitions. – syntaxerror Nov 21 '14 at 15:21
  • Of course, if you're really talking about find ... -exec mv ..., then you'll need to create ~/bin/mv (or some other appropriate directory) and have it do /bin/mv -i "$@" -- because find ... -exec doesn't look at aliases. – G-Man Says 'Reinstate Monica' Nov 21 '14 at 20:41
  • In this case, I'd prefer env mv. Less typing. :) As my local keyboard layout requires the SHIFT key to be pressed for a forward slash, I would always favor "slash-less" versions (if applicable). – syntaxerror Nov 22 '14 at 06:33
  • @syntaxerror: Well, of course you could create a script called ./mv_with_confirmation; but I’m responding to ayekat’s suggestion to create an alias for mv to mv -i (which would cause mv to be treated as mv -i automagically – at the bash prompt), and I’m saying that an alias won’t help if you’re doing -exec. (I.e., creating such an alias has the drawback that it lulls you into a false sense of security, thinking “I’ve fixed mv so it always asks for confirmation,” and then bites you when you do find … -exec mv.) … (Cont’d) – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:04
  • @syntaxerror: (Cont’d) … And I’m suggesting that creating a private mv *script* would work at the bash prompt and when run as a command by another program (e.g., find). And the standard procedure for creating/installing private programs/scripts is to put them into a private directory (~/bin is standard) and to put that directory at the beginning of your PATH, so you can type mv or find … -exec mv and get your private mv either way. … (Cont’d) – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:04
  • @syntaxerror: (Cont’d) … But, of course, the whole point of having a private copy of mv is to run /bin/mv with the -i option. But, if your private bin directory is at the beginning of your PATH (or anywhere ahead of /bin), then env mv will run your private mv, also. So, if you say env mv *in* your private mv, it will just invoke itself, creating an infinite loop. It will not be an effective way to invoke the real mv. – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:05
  • 1
    @syntaxerror: BTW, when you respond to comment (in a new comment), it’s conventional to mention the author’s name, preceded by “@”, as in “@G-Man”. That way I get notified. (I was able to respond to your last comment in a semi-timely manner because I got notified by Jenny D’s comment.) You can abbreviate, or use an entire name (without spaces), e.g., “@StéphaneChazelas”. The author of a post is automatically notified of comments to that post. See the Replying in comments paragraphs of this help page. – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:05
  • @G-Man Thanks, Sir for instructing a rookie like me. :) (Ugh, feels like back in school when the teacher spoke.) But to the point again: I actually meant env mv to be run in order to run the system's /bin/mv command (just like printf is a bash builtin and env printf will run the system version). But why you felt the need to have this simple issue turned to a 15-minute-long speech escapes me. :) – syntaxerror Nov 22 '14 at 21:23
  • @syntaxerror: Because your second comment didn't make any sense to me. Were you responding *to your own (first) comment*, or what? – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:26
  • @G-Man No! I'd never respond to my own comments (how silly would that be??) Again: I wondered throughout about this bit here: then you'll need to create ~/bin/mv (or some other appropriate directory) and have it do /bin/mv... Huh?? ~/bin/mv is in my $HOME, and /bin/mv is in the bin directory following / (the root directory). To me that's two entirely different pairs of socks. Where is the relation? – syntaxerror Nov 22 '14 at 21:29
  • @syntaxerror: I still don't understand what you're saying. – G-Man Says 'Reinstate Monica' Nov 22 '14 at 21:32
  • Then we'll have to leave it at that. Sorry 'bout that. This has been extended far enough now. – syntaxerror Nov 22 '14 at 21:34
  • 1
    I'm sorry, I was Internet-less for a day. @G-Man I agree that having a personal version of mv in a location at the beginning of $PATH would be a cleaner solution. On the other hand I primarly happen to mv carelessly in the interactive shell (because it happens fast). The moment I compose something more complex, like find or a for loop, or even a shell script, I tend to perform some dry runs (using echo) to ensure I don't break something. In those cases I don't need a hand-holding mv, because I'm already putting some thoughts into it. – ayekat Nov 23 '14 at 00:49
  • @ayekat Well, to be frank, I really don't understand how this answer got so many upvotes. I mean, what is the actual point of using mv -i in batch mode?! Clicking "y" 500 times? Who'd be as crazy as that wanting to confirm any single file copied in batch mode? And the line I had in my OP, find...-exec mv ..., IS a batch mode, since mv will be executed n times. In a school exam, I would have gotten a "missing the point" notation for that. – syntaxerror Dec 12 '14 at 05:30
  • @syntaxerror (1) my answer begins with "Additionally", (2) your problem can be narrowed down to your files being overwritten by subsequent calls to mv without any checks, and most importantly (3) mv -i does not ask for confirmation if nothing is being overwritten. If your loop/find is about to wreak havoc on your system, it will ask for confirmation, otherwise it will run quietly. – ayekat Dec 12 '14 at 10:58
  • @ayekat I can see that we're talking past each other. I was saying that I need batch mode, no matter what, and you came up with your great mv -i "solution" entirely unuseable for batch mode (Reason: see above). Moreover, clinging on "additionally" makes not much sense either, because if my line is about a batch-mode solution, I want no "additionally" for a non-batch-mode solution. So I will hold up to my opinion/conclusion that you've simply missed the point. When I go to the butcher's, and order pork, I don't want the butcher to tell me about beef as well and how great it is. – syntaxerror Dec 12 '14 at 13:31
  • @syntaxerror Well, I still fail to see how mv -i would be unsuited for your "batch mode", as mentioned in points (2) and (3) of my previous comment. But yes, I also think we're talking past each other, so I'll leave this discussion for good. – ayekat Dec 12 '14 at 15:18
  • @ayekat Oh well, is this so difficult to get? Because in batch mode, mv is executed n times! And if you execute mv -i also n times, you will also have to confirm mv -i n times for n files (as -i requires user confirmation). And I don't know about you, but with 250 files I would not want to confirm with y (=yes) 250 times. – syntaxerror Dec 12 '14 at 20:30
7

In addition to the excellent answers above, I'd like to clarify why you didn't get the question about whether to move the files or not.

If you move one file to a new name, and that name is not a directory, mv will rename your file to the new name.

The issue here is that you were using find to execute mv once per file, not once for all the files.

If, instead, you'd done mv *90.mp3 90, then mv would have failed with the error message that "the target file is not a directory".

Another piece of advice is to use tab completion when typing the target path. It will show you whether the target is a directory by adding / to the target name. You can also use mv -i to be asked whether you want to overwrite an existing file.

Jenny D
  • 13,172
  • If, instead, you'd done `mv 90.mp3 90, then mv would have failed with the error message that "the target file is not a directory".* Hah, yeah, why so complicated eh? I use your line and I'll be happy. Only in this trivial case, though. :) Because this is the normal way I ask my questions: I'd have them narrowed down for simplicity's sake. No one has objected tofindso far considering that the 90's files might as well be scattered around in __various subdirectories__ which I want to "catch" as well. If and *only* if they're *always* in one source directory, yourmv` line is applicable. – syntaxerror Nov 21 '14 at 09:30
  • @syntaxerror Very true - I meant this as an example of how mv behaves, not as a criticism of your choice of tools. – Jenny D Nov 21 '14 at 09:35
  • But even that criticism would be justified after all! Because I confess it might not be evident from my original post that in reality I'm trying to solve a more complex problem which I just broke down into a simple one to have the part solved which I can't solve on my own. This is because SE is not a "plzz do my homeworkz" site -- though often mistaken for that. (Unfortunately.) – syntaxerror Nov 21 '14 at 09:38
  • 1
    You could probably build something combining find and mv, e.g. find /music -type d -exec mv {}/*90.mp3 targetdir\; - but now I feel a bit like I'm overcomplicating it, and simply using -i or -t is more efficient – Jenny D Nov 21 '14 at 09:41
  • Indeed. But thanks for the tips in any case. :) Who knows who'd be needing them sometime. – syntaxerror Nov 21 '14 at 09:42
  • There's More Than One Way To Do It :-) – Jenny D Nov 21 '14 at 09:50
  • 1
    @Jenny: If your find . -type d -exec mv {}/*9?.mp3 target \; example worked, there would still be the risk that the mv command would look like mv file target for each directory that contained only one *9?.mp3 file; so all such files (except for the last one) would be lost. – G-Man Says 'Reinstate Monica' Nov 21 '14 at 20:35
  • 4
    @syntaxerror: I applaud your effort to expose the essential part of your problem, rather than the entire 42,000 line script in which it occurs. But, even if you did want to do something to all *.mp3 files in a directory tree, you could shopt -s globstar and then run your command on **/*.mp3 -- the ** will act like a find. – G-Man Says 'Reinstate Monica' Nov 21 '14 at 20:38
  • @G-Man Good points, both of them. I never even thought of shopt -s globstar, thanks for mentioning it! – Jenny D Nov 22 '14 at 10:15
1

As an alternate general-purpose strategy I'd like to suggest turning this kind of operation into a temporary script. I prefer to look at the results of find and turn them into a mv command by hand, ensuring that I understand what I'm doing before I execute. eg.

find . -name '* 9?.mp3' > tmp
vim tmp

Now I can look through a list of filenames and rewrite the file contents as a shell command.

  • Put the file contents on one line: ggVGJ
  • Prepend: Imv [esc]
  • Append: Asomedir/ [esc]
  • Save the file. Read it again. Take a breath.
  • Execute source tmp on the command line.

It is a conservative strategy, but I've been bitten too often by mistyped -exec or sed commands, or misunderstood shell expansions, and I prefer taking a slow consistent approach.

In other words: I am too cowardly to use -exec.

Tom Rees
  • 119
  • 1
    You left out the :%s/.*/"&"/ step -- because, in this case, you know that *every* filename contains at least one space. – G-Man Says 'Reinstate Monica' Nov 21 '14 at 20:44
  • 1
    This will bite you if the file names contain spaces or other special characters. You need to quote them properly. Reviewing a list of commands isn't particularly likely to catch errors. There are far better ways to review commands before running them, such as runing echo mv instead of mv, and then removing the echo if you're happy. – Gilles 'SO- stop being evil' Nov 21 '14 at 23:16
  • Spotting a mistake like filenames-with-spaces is precisely what this technique will help with :-) – Tom Rees Nov 21 '14 at 23:40
0

Another option:

-n, --no-clobber do not overwrite an existing file

it's the same as -i, but it wont ask, it'll fail.