LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Linux - Newbie (https://www.linuxquestions.org/questions/linux-newbie-8/)
-   -   Need some help with bash scripting. (https://www.linuxquestions.org/questions/linux-newbie-8/need-some-help-with-bash-scripting-4175441694/)

mainstream 12-16-2012 01:55 PM

Need some help with bash scripting.
 
Hello! :study:

Im trying to build and extend some scripts to reorganize my music collection.
I do have basic knowlodge of bash, but sometimes i run into errors, which i can't seem to solve on my own - which (i guess) would farly simple to solve.

My first question:
Code:

#!/bin/sh
DIR=${1:-/home/mainstream/Music/rename}
TYPE='.mp3'

addtags() {
echo id3v2 -D $1 #echo is only added for output
echo id3v2 -g Classical -y 2012 $1
}

for file in $(find $DIR -name \*$TYPE | head -10); do
  addtags $file
done

I want to pass the full (path +) name as parameter to the addtag function, so i can i rename it accordingly.

The output:
Code:

mainstream@dopamine-pc:~/Music$ ./foo.sh
id3v2 -D /home/mainstream/Music/rename/audo
id3v2 -g Classical -y 2012 /home/mainstream/Music/rename/audo
id3v2 -D machine
id3v2 -g Classical -y 2012 machine
id3v2 -D -
id3v2 -g Classical -y 2012 -
id3v2 -D redemption.mp3
id3v2 -g Classical -y 2012 redemption.mp3

I want the output to be "id3v2 -g Classical -y 2012 /home/mainstream/Music/rename/audo machine - redemption.mp3" - so not the 'scattered output above.


I tried adding " or ' but that trick doesn't seem to work though.
Can anyone help?

Thanks

mainstream 12-16-2012 02:05 PM

I know it has something to do with ` to escape the spaces, but i tried everything lol!

cbtshare 12-16-2012 03:07 PM

I'm not sure what you want to accomplish specifically here, so maybe you could show the output error and what you want the correct output to be

mainstream 12-16-2012 03:10 PM

Quote:

Originally Posted by cbtshare (Post 4850551)
I'm not sure what you want to accomplish specifically here, so maybe you could show the output error and what you want the correct output to be

Ok i edited the post the your liking. I want the output to be on one line, so i can pass the filename to id3v2 to change the tags.

colucix 12-16-2012 03:45 PM

The problem is that the list of arguments in the for statement is splitted by spaces (other than TABs and newlines, according to the default value of IFS) hence you will never see the complete file names inside the loop and the addtags function.

You can try to change field splitting behaviour, for example by setting IFS to newline:
Code:

#!/bin/sh
DIR=${1:-/home/mainstream/Music/rename}
TYPE='.mp3'

addtags() {
  echo id3v2 -D $1 #echo is only added for output
  echo id3v2 -g Classical -y 2012 $1
}

IFS='
'

for file in $(find $DIR -name \*$TYPE | head -10)
do
  addtags $file
done

I assume you use the /bin/sh invocation , as in your example. On the other hand, if you used the /bin/bash, you could try process substitution and leave IFS untouched:
Code:

#!/bin/bash
DIR=${1:-/home/mainstream/Music/rename}
TYPE='.mp3'

addtags() {
  echo id3v2 -D $1 #echo is only added for output
  echo id3v2 -g Classical -y 2012 $1
}

while read file
do
  addtags $file
done < <(find $DIR -name \*$TYPE | head -10)


cbtshare 12-16-2012 04:31 PM

the above code works, just unset IFS after.
Code:

#!/bin/sh

IFS='
'

DIR=${1:-/home/stain/KILLERS}
TYPE="*.mp3"

addtags() {
echo id3v2 -D $1 #echo is only added for output
echo id3v2 -g Classical -y 2012 $1
}

for file in $(find $DIR -name $TYPE | head -10); do
  addtags $file
done
unset IFS


chrism01 12-16-2012 05:08 PM

Unless there's more code, no need to unset IFS. It'll unset/revert when the script completes.

colucix 12-16-2012 05:25 PM

Well... actually the correct way is to save the default IFS and restore it later (if the script continues, as Chris notcied):
Code:

OLD_IFS="$IFS"
IFS='
'
#
# Use the new value of IFS here then reset the original:
#

IFS="$OLD_IFS"


mainstream 12-17-2012 09:51 AM

Great that worked! Thanks allot for the help guys! I appreciate it!

OK, this is what i made for now:
Code:

#!/bin/bash
OLD_IFS="$IFS"
IFS='
'

DIR=${1:-/home/mainstream/Music/rename}
TYPE="*.mp3"

read -e -p "Enter Year: " -i "2012" YEAR
echo "Select a Genre"
select GENRE in "Classical" "Dubstep" "Hip-Hop" "Skip"; do
    case $GENRE in
        Classical ) echo $GENRE; break;;
        Dubstep ) echo $GENRE; break;;
        Hip-Hop ) echo $GENRE; break;;
        Rap ) echo $GENRE; break;;
        Skip ) exit;;

    esac
done

addtags() {
    echo id3v2 -D $1 #echo is only added for output
    echo id3v2 -g $GENRE -y $YEAR $1
}

rename() {
    #break input filename into path and name
    #so that names with paths included are correctly handled
    filepath=${file%/*}
    newname=${file##*/}
    mp3=${file##*/}

    #remove junk characters from filename
    newname=${newname//+([,\'])/}

    regex='_\(final\)|\ \(CDQ\)|\ \(NoShout\)|\ \(CDQ \& Final\)|\ \(Dirty\)|\ \(Mastered\)|\ \(full\)'
    newname=${newname//+($regex)/}

    #replace delimiter
    regex='\ \-\ |\_\-\_|\-\-'
    newname=${newname//+($regex)/-}

    regex='_featuring_\|_feat_\|_f._\|_ft_|_feat._'
    newname=${newname//+($regex)/_ft._}

    regex='\(feat._|\(feat_'
    newname=${newname//+($regex)/\(ft._}

    #replace all instances
    newname=${newname//+(_and_)/_&_}

    #change spaces (and tabs) to underscores in filename
    #extglob reduces multiple spaces to single underscore
    newname=${newname//+([[:space:]])/_}

    #lowercase filename
    newname=${newname,,}

    #remove all initial "digit+[-_]" combinations
    #again extglob handles variable numbers of digits
    newname=${newname#+([0-9])[_-]}

    #remove any final "-zzzz" strings
    #similar to the last operation
    #assumes the file ends in ".mp3", however
    regex='zzzz|mst|alki|you|wus|htf|whoa'
    newname=${newname/-+($regex).mp3/.mp3}

    #remove any "-(nnn_bpm)" strings
    #remember, spaces have been changed to underscores already
    newname=${newname/-(+([0-9])_bpm)}

    #you can continue adding as many changes as you want here.

    #call mv to change the filename.
    #I used -iv to make it interactive and verbose, but that's up to you.
    if [[ $mp3 != $newname ]]
    then
          echo \.-------------------------------------------------------------\.
          echo "Old: \"$mp3\""
          echo "New: \"$newname\""
          echo mv $DIR/$mp3 $DIR/$newname
          echo \'-------------------------------------------------------------\'
    fi
}

for file in $(find $DIR -name $TYPE | head -10); do
    new_file=$(rename "$file")
    echo $new_file
    addtags $new_file
done

IFS="$OLD_IFS"

exit 0

Output is:
Code:

mainstream@dopamine-pc:~/Music$ bash foo.sh
Enter Year: 2012
Select a Genre
1) Classical
2) Dubstep
3) Hip-Hop
4) Skip
#? 1
Classical

id3v2 -D
id3v2 -g Classical -y 2012

It doesn't seem to pass to the rename() function?

mainstream 12-19-2012 08:35 AM

Nobody knows what's happening? :(

unSpawn 12-19-2012 09:07 AM

Quote:

Originally Posted by mainstream (Post 4852863)
Nobody knows what's happening? :(

Easiest way to debug this is to temporarily change the bang line from
Code:

#!/bin/bash
to
Code:

#!/bin/bash -vx
or set
Code:

set -vx
as the second line or execute the script as
Code:

/bin/bash -vx /path/to/script
BTW you really didn't need to mess with the IFS if you would have used a "while" instead of a "for" loop:
Code:

find $DIR -name $TYPE | head -10 | while read ITEM; do
    NEW_FILE=$(rename "${ITEM}")
    echo "${NEW_FILE}"
    addtags "${NEW_FILE}"
done


rtmistler 12-19-2012 09:48 AM

That was my recommendation which is to do things like unSpawn has suggested:

- Always look for ways to debug steps, turning on -vx gives you output and progress of your script.
- Set variables for the outcome of commands and check them, or specifically test the outcome of commands via $?
- Always verify on the target system the paths to commands. Since I mostly do embedded, the structure of the paths is deterministic and further, there are no guarantees that all commands are available. So from the shell on the target system I make sure the command exists, is accessible via $PATH, and that I know the exact location where the command is; such as /bin, or /sbin and include the full path to the command within the script.

And thanks to unSpawn, I hadn't known the various ways to control vx in scripts, I always add it as two lines:

Code:

set -x
set -v

And then comment those out or not depending on whether or not I need to debug.

mainstream 12-20-2012 10:18 AM

Thanks for your help, that worked quite good.
I was already using set -vx (use it always), but as i already stated earlier: my bash programming knowledge is quite basic :-(

David the H. 12-20-2012 10:47 AM

UnSpawn beat me to it. As always, the real solution is:

Don't Read Lines With For!

Seriously, it's such a common mistake, I think every time I come here I have to post this link 2 or 3 times.

Word-splitting and globbing expansion are always an issue any time you try to include parameter and command expansions (including importing data from files) inside other commands.

You only have two choices in such cases: you can keep everything together as a single argument by quoting them, or let the shell split it up everywhere it can. There's no completely safe way to split such data only where you want it to, except through use of a while+read loop, with null separators*.

A few other links along the same lines*

How can I read a file (data stream, variable) line-by-line (and/or field-by-field)?
http://mywiki.wooledge.org/BashFAQ/001


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


How can I rename all my *.foo files to *.bar, or convert spaces to underscores, or convert upper-case file names to lower case?
http://mywiki.wooledge.org/BashFAQ/030


( *Actually, a for loop on an array of individual lines/filenames would also work, but then the issue becomes first safely loading the array with the data. ;) )

David the H. 12-20-2012 11:32 AM

Incidentally, I noticed your interesting take on parameter substitution in your rename function. Since many of the patterns use the same format, I'd like to recommend a modification to remove a lot of that duplication.

(I'm only going to post the relevant part of the function)
Code:

rename() {
    ....

    local -a re_m re_r

    re_m=(
            '_(final)| (CDQ)| (NoShout)| (CDQ & Final)| (Dirty)| (Mastered)| (full)'
            ' - |_-_|--'
            '_featuring_|_feat_|_f._|_ft_|_feat._'
            '(feat._|(feat_'
            '_and_'
            '[[:space:]]'
          )

    re_r=(
            ''
            '-'
            '_ft._'
            '(ft._'
            '_&_'
            '_'
          )


    for i in "${!re_m[@]}" ; do

        newname=${newname//+("${re_m[i]}")/"${re_r[i]}"}

    done

    .....
}

Basically, store the find/replace patterns in matching arrays first and loop over them. You can expand on this as necessary, of course.

By the way, you don't generally need to backslash-escape special characters when you first store them in variables like this, as long as you properly quote the strings when setting them and when expanding them. Unless the strings you're working on actually have backslashes in them?

In other words, backslashes/quotes are only needed when protecting a character from direct shell parsing. Once a string is stored in a parameter, everything in it is treated literally, except for the word-splitting and globbing expansion that's done if unquoted. (To be specific, quotes and backslashes are processed before the parameter expansion step, but word-splitting and globbing happen after expansion).

Finally, you should endeavor to explicitly scope function variables locally whenever possible.


All times are GMT -5. The time now is 02:14 AM.