Is there a way to get the rename command to print JUST the output file name (not the XXX renamed as prefix or the brackets)? I'd rather not use regex unless that's the only choice.

Why I'm asking: I want to do several renames in succession on a single file (well, a list of files actually) so that the file names aren't ugly. It's difficult because if the first rename change it, there's no easy way to get the new name of the file that can become input to the second rename command.


This should rename files I've downloaded from the web that have %20 in them to . In a subsequent command in the same pipeline I might want to rename %28 as (. And I don't want to use a single rename command because of the regex complexity (if that's even possible).

find -iname "*%20*" | xargs -n 1 rename --some-option 's/%20/ /g' |  xargs -n 1 rename --some-option 's/%28/(/g'

  • As far as I know, the Perl rename command doesn't read from standard input, so you can't put it in a pipeline. If you could describe and show what it is you'd want to do, I'm sure someone would be able to give you some idea of what could be done. – Kusalananda Dec 19 '19 at 21:33
  • Sorry, I don't mean Perl's rename. I just mean this man7.org/linux/man-pages/man1/rename.1.html . But yes I will add an example. – Sridhar Sarnobat Dec 19 '19 at 21:34

Consider using a sequence of sed commands to transform the starting filename into the ultimate filename, then doing one mv to actually rename the file:

for i in *%*
    printf "mv -v '%s' '%s'\n" "$i" \
      "$( sed -e 's/%20/ /g' \
              -e 's/%28/(/g' \
         <<< "$i" )"

That command list actually doesn't change your files, which is a good thing. It just outputs the mv commands that you can use to actually do what you're trying to do.

Let's create some sample files containing %20 and %28 occurrences:

$ for i in foo bar farkle
> do
>     touch $i"%20".txt; touch $i"%28".txt; touch $i"%20-%28".txt
> done
$ ls -1

Now let's run the command list given at the outset:

$ for i in *%*
> do
>     printf "mv -v '%s' '%s'\n" "$i" \
>       "$(sed -e 's/%20/ /g' -e 's/%28/(/g' <<< "$i")"
> done
mv -v 'bar%20-%28.txt' 'bar -(.txt'
mv -v 'bar%20.txt' 'bar .txt'
mv -v 'bar%28.txt' 'bar(.txt'
mv -v 'farkle%20-%28.txt' 'farkle -(.txt'
mv -v 'farkle%20.txt' 'farkle .txt'
mv -v 'farkle%28.txt' 'farkle(.txt'
mv -v 'foo%20-%28.txt' 'foo -(.txt'
mv -v 'foo%20.txt' 'foo .txt'
mv -v 'foo%28.txt' 'foo(.txt'

If that sequence of mv commands looks correct to you, then up-arrow to run the command again, but this time add | sh at the end, to actually execute those mv commands instead of displaying them.

| improve this answer | |
  • Hmmmm, while that's not as elegant as in a perfect world, my scripts have become so ugly that this might be as good a solution as any. Thanks. – Sridhar Sarnobat Dec 19 '19 at 23:28
  • 1
    @SridharSarnobat If you wanted to, you could write the sed step as sed ... | sed ... | sed ... but chaining multiple -e 's/this/that/g' replacements does the same thing. I'll see if I can edit it to look a little nicer. – Jim L. Dec 20 '19 at 0:10
  • The more I think about this the more I like the pipeline of sed operations. – Sridhar Sarnobat Dec 30 '19 at 3:16

This seems to be, at it's core, an exercise in URL decoding, which is something of a solved problem.

Save this as, say, rename_script.sh:

DECODED="$(awk -niord '{printf RT?$0chr("0x"substr(RT,2)):$0}' RS=%.. <<< "${1}")"
[ ! -f "${1}" ] && echo "Source not found" && exit 1
[ -f "${DECODED}" ] && echo "Target already exists" && exit 1
[ "${1}" != "${DECODED}" ] && mv -i "${1}" "${DECODED}" || echo "Nothing to do"

And invoke thus:

find -name "*%*" -print0 | xargs -n1 -0 ./rename_script.sh

Heavy credit to this answer: https://stackoverflow.com/a/14000368/2858703

| improve this answer | |
  • 1
    Can you explain how the awk command works? It looks as if it requires GNU awk, right? – Kusalananda Dec 20 '19 at 19:18
  • This is useful for related cleansing, but I'd rather not make that assumption. There is other junk in my filenames. I'm actually using youtube-dl and my video filenames contain things like spaces before the dot extension, contain double extensions like .mp4.mkv. Some even contain emoji (which I'm trying to decide if I want to remove or not). – Sridhar Sarnobat Dec 20 '19 at 19:20
find . -type f -name '*%*' -execdir bash -c '
    name=${name//%20/ }    # replace each %20 by a space
    name=${name//%28/(}    # replace each %28 by a (
    mv "$1" "$name"' bash {} \;

This uses find to locate any regular file in or beneath the current directory that has a % character in their name. For each such file, a short bash script is executed which does the two substitutions that you mention on the given filename, and then renames the file.

It would be easy to add other parameter substitutions to this to delete or change other strings in the filenames.

A possibly quicker way would be to write a simple loop to do the same thing (assuming the bash shell for this particular variation):

shopt -s nullglob dotglob globstar

for pathname in ./**/*%*; do
    # skip pathnames to non-regular (or links to non-regular) files
    [[ ! -f "$pathname" ]] && continue

    name=${pathname##*/}    # strip off directory components
    name=${name//%20/ }     # ... so that we don't accidentally
    name=${name//%28/(}     # ... modify the directory path

    # the destination is made up by the directory path
    # (original pathname with filename stripped off) 
    # followed by the new name
    mv "$pathname" "${pathname%/*}/$name"

The nullglob shell option makes the pattern ./**/*%* expand to nothing if it doesn't match anything (it would ordinarily be left unexpanded), dotglob allows globbing patterns to mach hidden names, and globstar enables the use of ** to match down into subdirectories "recursively".

Adapting that second loop into something that uses find for generating the pathnames rather than a globbing pattern (e.g. for running under bash releases earlier than release 4, which didn't have **):

find . -type f -name '*%*' -exec bash -c '
    for pathname do
        name=${pathname##*/}    # strip off directory components
        name=${name//%20/ }     # ... so that we don't accidentally
        name=${name//%28/(}     # ... modify the directory path

        # the destination is made up by the directory path
        # (original pathname with filename stripped off) 
        # followed by the new name
        mv "$pathname" "${pathname%/*}/$name"
    done' bash {} +
| improve this answer | |
  • Ah another interesting solution, use regex replace in a shell variable reference (which I only found out about a few weeks ago but didn't think I'd ever use). – Sridhar Sarnobat Dec 19 '19 at 23:36
  • 2
    @SridharSarnobat This uses no regular expression. The only patters used are filename globbing patterns. – Kusalananda Dec 19 '19 at 23:37

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.