Bash adding a string to file name

2019-09-21 17:43发布

I need to add the word "hallo" at the end of each filename before extension in the given directory. but my code produces no output. the echo statements give no output and the file names are not changed

count=""
dot="."
for file in $ls
  do
    echo $file
    count=""
    for f in $file
    do
      echo $f
      if [ $f -eq $dot ];
      then
        count=$count"hallo"
      fi
      count=$count+$f
    done
    mv $file $count
  done

标签: bash macos shell
2条回答
爷、活的狠高调
2楼-- · 2019-09-21 18:17

@GeorgeVasiliou has shown how to do what you want; I'll take a stab at explaining what's wrong with your original script. There are a number of places where you're using the wrong syntax for what you're trying to do.

for file in $ls

This will expand a shell variable named ls, split its contents into words, expand any wildcards found in those words into lists of matching files, and then iterate over the result. But there's no ls variable defined, so it expands to nothing, and the loop never runs. I think you're trying to iterate over the output of the ls command; the syntax for that is for file in $(ls). But you shouldn't do that, because filenames can contain spaces and tabs and linefeeds and wildcards and all manner of other funny characters that can make this iterate over... something other than a list of filenames. See the BashFAQ entry 'Why you shouldn't parse the output of ls(1)'.

What you should use instead is for file in *. The * gets expanded to a list of matching files (all visible files in the current directory), and then that list is not messed with in any way, just iterated over directly. The only potential problem is that if there aren't any matches, expansion gets skipped, and the loop runs once with file set to "*".

echo $file

When a variable is used without double-quotes around it, it'll undergo that word splitting and wildcard expansion process I described above. Usually, nothing happens. Sometimes, weird and unexpected things happen. Use double-quotes, like echo "$file".

for f in $file

The for ... in ... statement iterates over words, not characters. The shell doesn't have a particularly good way to iterate over the characters in a string, but you can iterate over the character numbers with for ((i=0; i<${#file}; i++)); do and then get the individual characters with "${file:$i:1}".

Oh, and the for (( )) and ${var:start:length} constructs are bash extensions that aren't available in all shells. If you want to use either or both in your script, be sure to start it with a bash-specific shebang line (#!/bin/bash or #!/usr/bin/env bash).

if [ $f -eq $dot ];

Again, double-quote variable references to avoid chaos when the variables contain spaces or wildcards, or are blank. Also, in a [ ] test, the -eq operator does numeric comparison not string comparison; if you give it strings that aren't valid numbers, it'll give an error. For string comparison, use = instead. Also, a semicolon isn't needed at the end of the line (well, except for a few places like the end of a case). You'd use a semicolon here if the then was on the same line, but not if it's on the next line.

count=$count+$f

The + here will be inserted as a literal character in the variable value. To append two variables, just use something like count="$count$f". (Note: this is actually one of the few places where it's safe to leave the double-quotes off. But I recommend using them anyway, because it's hard to keep track of where it's safe and where it isn't, so it's much easier to just always double-quote your variables.)

mv $file $count

Again, double-quotes. Also, when running any sort of automated bulk rename/move like this, you should always use mv -n or mv -i to keep it from overwriting files if a naming conflict (or bug or whatever) occurs.

Actually, when I'm running a mass-mv script like this for the first time, I like to add echo before the active command (e.g. echo mv -n "$file" "$count") so I can do a dry run and make sure it's going to do what I expect, before I remove the echo and run it for real.

查看更多
Explosion°爆炸
3楼-- · 2019-09-21 18:25

With bash this can be done simplier like:

for f in *;do
echo "$f" "${f%.*}hallo.${f##*.}"
done

example:

$ ls -all
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file1.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file2.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file3.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file4.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file5.txt 

$ for f in *;do mv -v "$f" "${f%.*}hallo.${f##*.}";done
'file1.txt' -> 'file1hallo.txt'
'file2.txt' -> 'file2hallo.txt'
'file3.txt' -> 'file3hallo.txt'

$ ls -all
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file1hallo.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file2hallo.txt
-rw-r--r-- 1 29847 29847    0 Aug 21 14:33 file3hallo.txt

This works because ${f%.*} returns filename without extension - deletes everything (*) from the end (backwards) up to first/shortest found dot.

On the other hand this one ${f##*.} deletes everything from the beginning up to the longest found dot, returning only the extension.

To overcome the extensionless files problem as pointed out in comments you can do something like this:

$ for f in *;do [[ "${f%.*}" != "${f}" ]] && echo "$f" "${f%.*}hallo.${f##*.}" || echo "$f" "${f%.*}hallo"; done
file1.txt file1hallo.txt
file2.txt file2hallo.txt
file3.txt file3hallo.txt
file4 file4hallo
file5 file5hallo

If the file has not an extension then this will yeld true "${f%.*}" == "${f}"

查看更多
登录 后发表回答