0

Consider:

$ ls
foo  xyfooz.tex
$ find . -maxdepth 1 -type f -name '*foo*' -exec mv {} foo \;
$ ls ./foo*
xyfooz.tex

I'm trying to achieve the same sequence of instructions only with mv, not find:

$ ls
foo  xyfooz.tex
$ mv *foo* foo
mv: cannot stat '*foo*': No such file or directory
$ ls ./foo*
xyfooz.tex

Additionally (edit),

$ ls -F
foo/  xyfooz.tex*
$ mv -v *foo* foo

mv: cannot move 'foo' to a subdirectory of itself, 'foo/foo'
'xyfooz.tex' -> 'foo/xyfooz.tex'

How to make sure the 'stat' warning does not appear. In other words, I guess, I would I refine the pattern to limit the source to files not directories?

GNU bash, 4.3.48(1)-release (x86_64-pc-linux-gnu)

$ cat /etc/os-release
NAME="Linux Mint"
VERSION="18.3 (Sylvia)"
2
  • It's not clear what is going on, but shell expansion can't be limited to files: you need to use find or for f in *foo*; do [ -f "$f" ] && mv "$f" foo; done. Please update your question with the results on repeating both tests using ls -F each time and mv -v in both the find ... -exec and the stand-alone command. I'm using Ubuntu 16.04.04 with GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu). If foo is a directory and xyfooz.tex a file, my stand-alone mv gives mv: cannot move 'foo' to a subdirectory of itself, 'foo/foo', which is as expected. What OS are you using?
    – AFH
    Commented Apr 16, 2018 at 1:27
  • I have made edits according to your suggestion, but they don't tell us anything new. I'm using the same bash version, that was already in the question. I would have hoped there existed a regex way to restrict the source to type file.
    – Erwann
    Commented Apr 16, 2018 at 19:00

1 Answer 1

2

With mv

cannot move 'foo' to a subdirectory of itself is harmless in this case, you can ignore it. I mean mv will move the rest despite the warning. However the exit status will not be 0, this may be a great obstacle in scripts where you want to abort or to take a correcting action if mv doesn't succeed.

This has been already said in a comment: you can silence mv by redirecting stderr with 2>/dev/null; other warnings or errors will be redirected as well though.

I think you cannot easily "refine the pattern to limit the source to files not directories". Still you can take an approach that doesn't match the literal foo. The following approach has nothing to do with files or directories; just with names, because globs deal with names; so it may not be enough in some cases.

The *foo* glob matches four disjunctive kinds of objects:

  1. foo itself
  2. foo with prefix only, like bar-foo – you can match them by *?foo
  3. foo with prefix and postfix, like bar-foo-baz*?foo?*
  4. foo with postfix only, like foo-bazfoo?*

To exclude foo you need the latter three (2-4) only, so the first approach may be:

mv *?foo *?foo?* foo?* foo/

Unless you have the nullglob shell option set, any pattern needs to match something, otherwise it will be passed to mv literally. You don't want this. The solution is to run the following command beforehand:

shopt -s nullglob

This shell option will make any unmatched glob expand to nothing. I advise you to research dotglob option as well.

Note that *?foo* matches (2-3). Similarly *foo?* matches (3-4). Additionally consider -- to tell mv that all following arguments aren't options. This way a file like --foo won't be interpreted as an (unknown) option. This leads to two better commands:

mv -- *?foo* foo?* foo/

or

mv -- *?foo *foo?* foo/

It doesn't matter which one you choose. Just don't use mv *?foo* *foo?* foo/; it will pass (e.g.) bar-foo-baz to mv twice (i.e. as two separate arguments), thus generating an error when the tool tries to move this object for the second time.

When no match is found at all, my mv commands will degenerate themselves to mv foo/ and throw an error. Your original mv command (with nullglob unset) would get the literal *foo* and throw another error.


Example console session:

$ shopt -s nullglob
$ mkdir foo
$ touch bar-foo bar-foo-baz foo-baz xyfooz.tex ./--foo
$ mv -v -- *?foo* foo?* foo/
'bar-foo' -> 'foo/bar-foo'
'bar-foo-baz' -> 'foo/bar-foo-baz'
'--foo' -> 'foo/--foo'
'xyfooz.tex' -> 'foo/xyfooz.tex'
'foo-baz' -> 'foo/foo-baz'
$ 

Simple hack

Let's say dummy doesn't exist.

mv foo dummy
mv *foo* dummy/
mv dummy foo

Renaming a directory should be very fast in inode-based filesystems. Programs having files from within foo/ already opened shouldn't interfere nor break because it's the inode that matters. However if any program needs to open a file (by its path including foo/) between the first and the third mv, it will fail. Other side effects may occur (imagine a third party program recreating foo/ in the meantime), that's why I call this approach a hack. Think twice before you use it.


With find anyway

Telling apart files from directories is a job for find. What you did with find is basically the right way. What you want to do (and what I did above) with mv and shell globbing seems… well, "less right" at most. This is your command:

find . -maxdepth 1 -type f -name '*foo*' -exec mv {} foo \;

This find will run a separate mv for every matched file, the whole command will perform poorly. For this reason one may want to switch to a single mv. If this is your line of thought (and your mv and find are rich enough) then you should consider this "very right" way:

find . -maxdepth 1 -type f -name '*foo*' -exec mv -t foo/ -- {} +

The advantages over your find command and/or over plain mv with glob(s):

  • a single mv may get multiple files to work with;
  • still, if there are too many files for one command line to handle, find will run additional mv process(es);
  • in a case when no file matches the pattern, mv won't be run at all;
  • thanks to -- a file like --foo will not be interpreted as an (unknown) option to mv;
1
  • Comprehensive, just wanted to say thanks, will look into it.
    – Erwann
    Commented May 6, 2018 at 0:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .