I need to rename a batch of markdown files with random names based on the title name, and the destination name need to be with spaces (this is a spec). I'm trying this one-liner

for i in *;
do mv $i $(grep \# $i | head -1 | sed 's/# //' | sed 's/[_\?\!\]//' | sed 's/[:/]/ - /') ; 

but I receive the "the targer X is not a directory" error in the spaced name.

Any tips?


The main issue is that the command substitution is not quoted, so it gets split into distinct arguments and each argument subject to pathname expansion (that latter part in bash, but not zsh), and when mv sees more than two, it assumes the last one is a directory to move the files to.

For the same reason, $i itself should be quoted (in bash), and to avoid problems if some file name starts with a dash -, you should use ./* in place of * in the pattern, or put the end-of-options marker -- in the mv command.

Also, I think we could simplify the way you process the filenames a bit. To start with, sed can take multiple commands, so you only need to call it once. You also probably want to add /g to the end of the s/// commands, to make the substitutions for all matches, not just the first. Also note that 2 of those 3 backslashes in [_\?\!\] are redundant. Use [_?!] if you want to match on _, ? or ! or [_\?!] if you also want to include backslash (though [_\\?!] would make it more foolproof with some utilities).

for f in ./*; do
    mv -- "$f" "$(grep \# "$f" | head -1 | sed -e 's/# //g' -e 's/[_?!]//g' -e's/[:/]/ - /g')"

You could also combine the grep and head into the sed too:

for f in ./*; do
    mv -- "$f" "$(sed -ne '/^# / {s///; s/[_?!]//g; s/[:/]/ - /g; p; q;}' < "$f")"

Here, the commands inside {} are conditional on the pattern /^# /, and q at the end exits the script at the end of that block, so only the first matching line is processed.

Similarly in awk:

awk '/^# / { sub(/# /, ""); gsub(/[_?!]/, ""); gsub(/[:/]/, " - "); print; exit }' < "$f"

I might also change the last replacement to s/[:/] */ - /, so that a string like blah: bleh would turn into blah - blah, instead of blah - bleh (with two spaces after the dash).


Just add quotes around the target name:

 mv $i "$(grep \# $i | head -1 | sed 's/# //' | sed 's/[_\?\!\]//' | sed 's/[:/]/ - /')" ; 
  • Combine the three calls to sed? Or do it all with awk? – Kusalananda Feb 20 at 14:12
  • or sed -e 's/# //' -e 's/[_\?\!\]//' -e 's/[_\?\!\]//' – Archemar Feb 20 at 14:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.