4

Consider we have many photos with names like DSC_20170506_170809.JPEG. To rename the photos so that they follow the pattern Paris_20170506_170809.JPEG, I've wrote the following script that works perfect.

for file in *.JPEG; do mv ${file} ${file/DSC/Paris}; done 

My question is , how we can write this script using a while loop instead of a for loop?

2
  • 10
    I see no reason whatsoever to convert this into a while loop.
    – Kusalananda
    CommentedNov 2, 2017 at 10:06
  • 1
    I'm a Bash newbie and try to learn more about bash by practicing. I would be grateful if anyone suggests an alternative using while loop.
    – sci9
    CommentedNov 2, 2017 at 10:35

3 Answers 3

7

There's nothing wrong with using a while loop here. You just have to do it right:

set -- *.jpeg while (($#)); do mv -- "${1}" "${1/DSC/Paris}" shift done 

The while loop above is just as reliable as the for loop (it will work with any file names) and while the latter is - in many instances - the most appropriate tool to use, the former is a valid alternative1 that has its uses (e.g. the above could process three files at a time or process only a certain number of arguments etc).


All these commands (set, while..do..done and shift) are documented in the shell manual and their names are self-explanatory...

set -- *.jpeg # set the positional arguments, i.e. whatever that *.jpeg glob expands to while (($#)); do # execute the 'do...' as long as the 'while condition' returns a zero exit status # the condition here being (($#)) which is arithmetic evaluation - the return # status is 0 if the arithmetic value of the expression is non-zero; since $# # holds the number of positional parameters then 'while (($#)); do' means run the # commands as long as there are positional parameters (i.e. file names) mv -- "${1}" "${1/DSC/Paris}" # this renames the current file in the list shift # this actually takes a parameter - if it's missing it defaults to '1' so it's # the same as writing 'shift 1' - it effectively removes $1 (the first positional # argument) from the list so $2 becomes $1, $3 becomes $2 and so on... done 

1: It's not an alternative to text-processing tools so NEVER use a while loop to process text.

2
  • Thank you for answer. Could you please interpret your code line by line to understand what this script does?
    – sci9
    CommentedNov 3, 2017 at 4:44
  • 1
    @sci9 - see editCommentedNov 3, 2017 at 14:47
4

for-loops are usually done over static data. That is, data that will not change over the course of the loop, or over a known interval.

while-loops are usually used when it is not known how many iterations will be needed, like prompting a user for input and validating the response until it's correct, or looping over the data read from file or a pipe.

In this case, you are looping over filenames read directly from the current directory, given to the loop by the shell expanding a file globbing pattern. A for loop is the correct looping construct to use. A while loop would only complicate matters and make the code harder to read and to get right.

This would be one example of a while loop doing the same thing as your for loop:

printf '%s\n' DSC*.JPEG | while read -r name; do [ ! -f "Paris${name#DSC}" ] && mv "$name" "Paris${name#DSC}" done 

There are issues here:

  1. The filenames can't contain newlines since the loop reads newline-separated names. We could get around this by getting printf to output \0-separated filenames and read to read these, but that would make it even more complicated (and non-portable).
  2. There is no gain whatsoever. The list of names is still static and we've introduced a redundant step to pass these names from printf to the loop, a step that brought issues with what characters we can allow the filenames to contain (see previous point).

Really, the only thing wrong with your code is that the variable expansions are unquoted.

3
  • Also note that read would strip the trailing G off the file name if $IFS so happened to contain G. With an unmodified $IFS, you'd have issues with leading and trailing space or tab characters (and newline with NUL delimited data). The syntax to read a line of input verbatim is IFS= read -r lineCommentedNov 3, 2017 at 15:04
  • There's also a missing -- in the OP's code.CommentedNov 3, 2017 at 15:05
  • Another problem with that while loop is that mv's stdin is redirected to a pipe (which matters in case mv prompts the user).CommentedNov 3, 2017 at 15:06
4

You can also use an Bash until loop if you wish.

set -- *.jpeg until ((! $#)) do mv -- "${1}" "${1/DSC/Paris}" shift done 
0

    You must log in to answer this question.

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.