LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Filenames with spaces in bash scripts. (https://www.linuxquestions.org/questions/programming-9/filenames-with-spaces-in-bash-scripts-4175450995/)

stf92 02-20-2013 01:52 PM

Filenames with spaces in bash scripts.
 
Code:

#list.sh

#!/bin/sh

list="$(ls *mp3)"
n=0
for file in $list
do
        v[n]="$file"
        let n=n+1
done

n=0
until [ -z "$1" ]
do
        echo "${v[$1]}"
        shift
let n=n+1
done

exit

This script takes an argument n and prints the name of the n+1 th file in the current directory. If given several arguments, for example
Code:

$ list.sh 0 3 4
it prints the 1st, 4th and 5th filename in the directory. The problem is that there may be spaces in the filenames. If the 1st file is "john smith", then 'list.sh 1' will not print the 2nd file name but "smith". The problem is in the first half of the script, the repeat block can be disregarded. How can I solve this?

colucix 02-20-2013 02:03 PM

I think you should definitively read these:
http://mywiki.wooledge.org/BashPitfa...8ls_.2A.mp3.29
http://mywiki.wooledge.org/ParsingLs
http://mywiki.wooledge.org/BashFAQ/020
The first two articles explain what and why is wrong. The last one proposes solutions.

suicidaleggroll 02-20-2013 02:26 PM

Code:

list="$(ls *mp3)"
for file in $list

should be

Code:

for file in *mp3

propofol 02-20-2013 02:27 PM

One option may be to read the list of files directly into an array with one file per line:

Code:

IFS=$'\n'
list=($(ls -1 *.mp3))
# or use $(find ~/mp3folder/ -iname '*.mp3' -print) for a recursive search
IFS=''

n=0
for file in "${list[@]}"
  do
  echo "$file"
  ((n++))
done

Regards,
Stefan

stf92 02-20-2013 02:56 PM

Thank you very much. The links solved the problem. Could I ask you how to determine, within a bash script, if a certain string contains a given substring. For example, given "superior" I want to know it if contains "upe", which here is certainly true.

More precisely, the problem is this: one of the arguments can be of the form m-n, for example,
Code:

list.sh 2 4 6-8
The program would have to know the last argument contains a hyphen (it means play from 6 up to 8) and get the 6 and the 8. Is this very difficult? Once the presence of the hyphen is detected, perhaps sed could extract m and n.

colucix 02-20-2013 03:24 PM

You can try the pattern matching operator =~. In order to match a numeric interval, looking for an hyphen is not enough, otherwise strings like "-8" or "hello-world" or "6-" would be valid. Instead you have to match the numbers as well:
Code:

[[ $1 =~ ^[0-9]+-[0-9]+$ ]] && echo "$1 is a numeric interval"

stf92 02-20-2013 03:49 PM

Cool! Then I could do
Code:

s1=$(echo $1|sed s/"-"/" ")
Having now a blank separator between the two numbers I guess it would be easy to put each one in a separate variable, though I do not immediately see how to do it.

stf92 02-20-2013 10:05 PM

I did it this way:
Code:

# play.sh

# Example: play.sh 0 3-5 8
# Play 1st, 4th, 5th, 6th and 9th files in current directory.
#!/bin/sh

n=0
for file in *mp3
do
        v[n]="$file"
        let n=n+1
done

n=0
until [ -z "$1" ]
do
       
        if [[ $1 =~ ^[0-9]+-[0-9]+$ ]]  # Thanks colucix
        then
                # $1 is an interval m-n
                int=$1
               
                # Split "m-n" in "m" and "n"
                n1="${int%-*}"    # Get m
                n2="${int##*-}"  # Get n
               
                # Process from v[n1] up to v[n2]
                for (( i=$n1; i <= $n2; i++ )); do
                        mplayer "${v[i]}"
                done
        else
                mplayer "${v[$1]}"
        fi
        shift
        let n=n+1
done

exit

It works.

grail 02-21-2013 02:36 AM

Now you have a solution .. here is another way to think about :)
Code:

#!/bin/bash

files=( *.mp3 )

for f
do
        if [[ $f =~ - ]]
        then
                for (( i = ${f%-*}; i < ${f#*-}; i++ ))
                do
                        mplayer "${files[i]}"
                done

                f=${f#*-}
        fi

        mplayer "${files[f]}"
done

I would probably add in a test to make sure that the directory where you run this actually has mp3 files, ie test if array empty.

Many others of course ... just thought it might interest you.

stf92 02-21-2013 03:42 AM

I like it. It seems more compact.

stf92 02-21-2013 03:58 AM

If I press ^C while the script is running it has no other effect than making mplayer go on to the next song. How could I stop the program in response to ^C or some other key?

grail 02-21-2013 08:32 AM

Probably need to investigate the trap and / or wait command.

mina86 02-21-2013 08:48 AM

Quote:

Originally Posted by propofol (Post 4896211)
One option may be to read the list of files directly into an array with one file per line:

Code:

IFS=$'\n'
list=($(ls -1 *.mp3))
# or use $(find ~/mp3folder/ -iname '*.mp3' -print) for a recursive search
IFS=''

n=0
for file in "${list[@]}"
  do
  echo "$file"
  ((n++))
done


No. This is incorrect. File names can have new line characters in them.

Quote:

Originally Posted by stf92 (Post 4896225)
More precisely, the problem is this: one of the arguments can be of the form m-n, for example,
Code:

list.sh 2 4 6-8

Code:

foo=6-8
range=false
case $foo in
*-*-*)
    echo "$foo: invalid argument" >&2
    exit 1
    ;;
?*-?*)
    foo1=${foo%-*}
    foo2=${foo#*-}
    ;;
*-*)
    echo "$foo: invalid argument" >&2
    exit 1
esac

if $range; then
    : use $foo1 and $foo2
else
    : use $foo
fi


propofol 02-22-2013 01:06 AM

Quote:

Originally Posted by mina86 (Post 4896708)
No. This is incorrect. File names can have new line characters in them.

You are correct but how many files do you have on your system with '\n' in the name? If you want to be pedantic: find with '-print0' & IFS=$'\0'

mina86 02-22-2013 08:54 AM

Quote:

Originally Posted by propofol (Post 4897242)
You are correct but how many files do you have on your system with '\n' in the name?

Irrelevant. Assuming that files with new line characters do not exist is incorrect and may lead to security bugs.

David the H. 02-24-2013 06:30 PM

I'm usually a stickler for this kind of thing, but I actually think it doesn't matter too much to worry about filenames with newlines in them, particularly when working directly on the command line or in other quick&dirty situations.

That said, when writing a script that will be used repeatedly, it is generally better to do it correctly the first time. Then you'll never have to worry about correcting it later.

The main issue I have with propofol's script is here:

Code:

list=($(ls -1 *.mp3))
This is a completely useless use of ls, even considering that IFS was changed to avoid the parsing ls problem. All ls is doing here is acting as a glorified echo, since the shell is expanding the filenames into an argument list anyway before it even runs.

I will also say that changing IFS globally is generally bad form, and can lead to non-transparent code and hard-to-diagnose errors.

As usual, simple globbing is all you generally need, either to set an array, or processed directly in a for loop.

Code:

list=( *.mp3 )
The use of find is a bit more problematic, as it would depend on word splitting to work properly in the above. For that you should avoid command substitution entirely and use a while+read loop, with null separators.

Code:

while IFS='' read -r -d '' fname; do
    list+=( "$fname" )
done < <( find ~/mp3folder/ -iname '*.mp3' -print0 )


How can I find and deal with file names containing newlines, spaces or both?
http://mywiki.wooledge.org/BashFAQ/020


One more point that hasn't been mentioned yet. Never use #!/bin/sh as the shebang unless you specifically need POSIX-compatible portability. If you intend to rely on Bash specific features like arrays, then you need to call it explicitly with #!/bin/bash. Otherwise it won't work properly if the system you run it on doesn't have bash as its system shell.


All times are GMT -5. The time now is 07:17 PM.