bash string to array with spaces and extra delimiters - arrays

I'm trying to create arrays from strings that have pipe ("|") as delimiters and include spaces. I've been looking around for a while and I've gotten close thanks to sources like How do I split a string on a delimiter in Bash?, Splitting string into array and a bunch more. I'm close but it's not quite working. The two main problems are that there are spaces in the strings, there are starting and ending delimiters, and some of the fields are blank. Also, instead of just echoing the values, I need to assign them to variables.
Here's the format of the source data:
|username|full name|phone1|phone2|date added|servers|comments|
Example:
|jdoe | John Doe| 555-1212 | |1/1/11 | workstation1, server1 | added by me |
Here's what I need:
Username: jdoe
Fullname: John Doe
Phone1: 555-1212
Phone2:
Date_added: 1/1/11
Servers: workstation1, server1
Comments: guest account
Edit: I use sed to strip out the first and last delimiter and spaces before and after each delimiter, input is now:
jdoe|John Doe|555-1212||1/1/11|workstation1, server1|added by me
Here's things I've tried:
oIFS="$IFS"; IFS='|'
for line in `cat $userList`; do
arr=("$line")
echo "Username: ${arr[0]}" #not assigning a variable, just testing the output
echo "Full Name: ${arr[1]}"
echo "Phone 1: ${arr[2]}"
echo "Phone 2: ${arr[3]}"
# etc..
done
IFS="$oIFS"
Output:
Username:
Full Name:
Phone 1:
Phone 2:
Username: jdoe
Full Name:
Phone 1:
Phone 2:
Username: John Doe
Full Name:
Phone 1:
Phone 2:
Another thing I tried:
for line in `cat $userList`; do
arr=(${line//|/ })
echo "Username: ${arr[0]}"
echo "Full Name: ${arr[1]}"
echo "Phone 1: ${arr[2]}"
echo "Phone 2: ${arr[3]}"
# etc
done
Output:
Username: jdoe
Full Name: John
Phone 1:
Phone 2:
Username: Doe
Full Name: 555-1212
Phone 1:
Phone 2:
Any suggestions? Thanks!!

Your first attempt is pretty close. The main problems are these:
for line in `cat $userList` splits the file by $IFS, not by line-breaks. So you should set IFS=$'\n' before the loop, and IFS='|' inside the loop. (By the way, it's worth noting that the for ... in `cat ...` approach reads out the entire file and then splits it up, so this isn't the best approach if the file can be big. A read-based approach would be better in that case.)
arr=("$line"), by wrapping $line in double-quotes, prevents word-splitting, and therefore renders $IFS irrelevant. It should just be arr=($line).
Since $line has a leading pipe, you either need to strip it off before you get to arr=($line) (by writing something like $line="${line#|}"), or else you need to treat arr as a 1-based array (since ${arr[0]}, the part before the first pipe, will be empty).
Putting it together, you get something like this:
oIFS="$IFS"
IFS=$'\n'
for line in `cat $userList`; do
IFS='|'
arr=($line)
echo "Username: ${arr[1]}" #not assigning a variable, just testing the output
echo "Full Name: ${arr[2]}"
echo "Phone 1: ${arr[3]}"
echo "Phone 2: ${arr[4]}"
# etc..
done
IFS="$oIFS"
(Note: I didn't worry about the fields' leading and trailing spaces, because of the "I can do that step separately" part . . . or did I misunderstand that? Do you need help with that part as well?)

IFS='|'
while read username fullname phone1 phone2 dateadded servers comments; do
printf 'username: %s\n' "$username"
printf 'fullname: %s\n' "$fullname"
printf 'phone1: %s\n' "$phone1"
printf 'phone2: %s\n' "$phone2"
printf 'date added: %s\n' "$dateadded"
printf 'servers: %s\n' "$servers"
printf 'comments: %s\n' "$comments"
done < infile.txt

Another solution:
shopt -s extglob
infile='user.lst'
declare -a label=( "" "Username" "Full Name" "Phone 1" "Phone 2" )
while IFS='|' read -a fld ; do
for (( n=1; n<${#label[#]}; n+=1 )); do
item=${fld[n]}
item=${item##+([[:space:]])}
echo "${label[n]}: ${item%%+([[:space:]])}"
done
done < "$infile"
Leading and trailing blanks will be removed.

Using arrays and paste. Doesn't account for empty fields since OP said it's not a requirement.
userList='jdoe|John Doe|555-1212||1/1/11|workstation1, server1|added by me'
fields=("Username: " "Full Name: " "Phone 1: " "Phone 2: " "Date_added: " "Servers: " "Comments: ")
IFS='|' read -ra data <<<${userList}
paste <(IFS=$'\n'; echo "${fields[*]}") <(IFS=$'\n'; echo "${data[*]}")
Username: jdoe
Full Name: John Doe
Phone 1: 555-1212
Phone 2:
Date_added: 1/1/11
Servers: workstation1, server1
Comments: added by me

Use column if available to you.
readarray -t my_vals <<< $(seq 5)
echo "${my_vals[#]}" #1 2 3 4 5
column -to: <<< "${my_vals[#]}" #1:2:3:4:5
-t = Table Output
-o = Output Delimiter (set to ':' here)

Related

How to substitute array element with a variable in bash?

I've got about 10 arrays like so:
array_1=("Mike" "George" "Sam" "1234" "5678")
array_2=("Albert" "Isabel" "Sami" "4567" "9821")
array_3=("Michel" "Tom" "Cathy" "321" "5664")
array_4=("name 1" "name 2" "name 3" "1233" "4567")
array_5=...
To get single array elements (this is needed because not all are used in the script):
name1="${array_1[0]}"
name2="${array_1[1]}"
name3="${array_1[2]}"
number1="${array_1[3]}"
number2="${array_1[4]}"
Sometimes I want to use array_2 (or 3/4..) instead of array_1. To avoid replacing (array_1) in all the lines of the names and numbers, I'm looking to use a simple variable substitution, so tried to replace with different kind of quotes, including:
myarray="array_1" // also tried 'array_1' and $array_1
name1="${myarray[0]}" // also tried "${$!myarray[0]}" and different quotes combinations
At this point I'm a bit confused about how bash quotes and probably indirects may work for this example, none the found answers nor various tries worked so far, aiming to see if there is rather a simple approach to address this or should the way of how arrays are being used here needs to be changed. Any hint is appreciated.
You need to make myarray a nameref with declare -n.
declare -n myarray=array_1
Then you reference it as if it were the named array:
$ echo "${myarray[0]}"
Mike
Note that this only works in bash 4.3+, however. Apple is allergic to GPL v3+ so macOS ships with 3.2 in /bin; if you're on a Mac, you'll need to install a newer version (e.g. with MacPorts or Homebrew).
If your bash is too old for namerefs you can use indirection:
array=array_1
name1="${!array[0]}"
name2="${!array[1]}"
name3="${!array[2]}"
number1="${!array[3]}"
number2="${!array[4]}"
Demo:
$ array=array_1
$ name1="${!array[0]}"
$ echo "$name1"
Mike
$ array=array_2
$ name1="${!array[0]}"
$ echo "$name1"
Albert
#Mike
Proceed as per comment from Mark Reed.
I thought of sharing:
    $ cat "73180037.sh"
    #!/bin/bash
    array_1=("Mike" "George" "Sam" "1234" "5678")
    array_2=("Albert" "Isabel" "Sami" "4567" "9821")
    array_3=("Michel" "Tom" "Cathy" "321" "5664")
    array_4=("name 1" "name 2" "name 3" "1233" "4567")
    array_5=...
    for i in {1..5}
    do
        name1=array_$i[0];
        name2=array_$i[1];
        name3=array_$i[2];
        number1=array_$i[3];
        number2=array_$i[4];
        echo ${!name1} ${!name2} ${!name3} ${!name3} ${!number1} ${!number2}
    done
You kind of need, an array of array which does not supported in bash but it does not mean you cannot make it
Here is a dirty way of having an array of array with quick access
script
#!/bin/bash
# bash strict mode
set -C
set -Ee
set -T
set -u
set -o pipefail
# global array
declare -a arr=();
# get each line as an array, start from 0
function array_of_array(){
# get the index
declare -r index=${1:?'Error: first arg is needed'};
# each line is an array
# delimiter is comma ,
data="name surname 11, name surname 12, 123, 456
name surname 21, name surname 22, 123, 456, 789
name surname 31, name surname 32, 123, 456";
# extract that line, start from 0
mapfile -s $index -t array <<< "$data";
# delimiter is ,
IFS=',';
# remove space after comma
array=${array//, /,}
# save it in arr, a global variable
read -a arr <<< "${array[0]}";
}
# read first line into an array
# read -a arr <<< "$(array_of_array 0)"
array_of_array 0
echo
echo '### first ###'
echo arr: "${arr[0]}"
echo arr: "${arr[1]}"
echo arr: "${arr[2]}"
echo arr: "${arr[3]}"
echo size: "${#arr[#]}"
# read second line into an array
# read -a arr <<< "$(array_of_array 1)"
array_of_array 1
echo
echo '### second ###'
echo arr: "${arr[0]}"
echo arr: "${arr[1]}"
echo arr: "${arr[2]}"
echo arr: "${arr[3]}"
echo arr: "${arr[4]}"
echo size: "${#arr[#]}"
sample output
### first ###
arr: name surname 11
arr: name surname 12
arr: 123
arr: 456
size: 4
### second ###
arr: name surname 21
arr: name surname 22
arr: 123
arr: 456
arr: 789
size: 5

Concatenating a string from an array

I have a full names that have been read into arrays. I am trying to create a file using only the last name; the last name might have white spaces that should be replaced by underscores. My thought is to create a string of the file name and then create the file. I have already taken care of the cases with only one last name. I am having trouble with the last names with white spaces. This is what I have so far:
if [ "${#name_arr[#]}" -eq 2 ]; then
for i in "${name_arr[#]:1}";do # :1 because first element is their first name
last_name=$i
done
echo $last_name
else
for i in "${name_arr[#]:1}";do
last_name=${last_name}_${i}
done
echo $last_name
fi
The output of this concatenates all of the names with underscores. So instead of:
Doe
Austen
Vaughn_Williams
Doe
It is echoing:
Doe
Austen
Austen_Vaughn_Williams
Doe
You don't need loops, or nor do you need to check the length of the list. Just join all but the first element with a space to get the last name.
last_name=${name_arr[*]:1} # Assuming the default value of IFS
last_name=${last_name// /_}
At the cost of a fork, you can do this in one line.
last_name=$(IFS='_'; echo "${name_arr[*]:1}")
Try this approach
if [ "${#name_arr[#]}" -eq 2 ]; then
for i in "${name_arr[#]:1}";do # :1 because first element is their first name
last_name=$i
done
echo $last_name
else
last_name=${name_arr[1]}
for i in "${name_arr[#]:2}";do
last_name=${last_name}_${i}
done
echo $last_name
fi
First, take the 2nd element of the name_arr in the last_name, and add the remaining elements of the array in to the last_name variable with a loop

Use array elements as names of the variables i want to read to from a file

I know that reading a .csv file can be done simply in bash with this loop:
#!/bin/bash
INPUT=data.cvs
OLDIFS=$IFS
IFS=,
[ ! -f $INPUT ] && { echo "$INPUT file not found"; exit 99; }
while read flname dob ssn tel status
do
echo "Name : $flname"
echo "DOB : $dob"
echo "SSN : $ssn"
echo "Telephone : $tel"
echo "Status : $status"
done < $INPUT
IFS=$OLDIFS
But I want to slightly modify this- I want to make the columns be defined by the programmer in the bash file.
For example:
declare -a columns=("Name", "Surname", "ID", "Gender")
while read columns
do
//now echo everything that has been read
done < $INPUT
So I want to specify the list of variables that should be used as the container to the read CSV data with an array and then access this array inside the while body.
Is there a way to do it?
The key to this solution is the comment before the while statement below. read is a built-in, but it is still a command, and command arguments are expanded by the shell before executing the command. After expansion of ${columns[#]}, the command becomes
read Name Surname ID Gender
Example:
# Don't use commas in between array values (since they become part of the value)
# Values not quoted because valid names don't need quotes, and these
# value must be valid names
declare -a columns=(Name Surname ID Gender)
Then, we can try:
# Read is a command. Arguments are expanded.
# The quotes are unnecessary but it's hard to break habits :)
while read "${columns[#]}"; do
echo Name is "$Name"
# etc
done <<< "John Doe 27 M"
Output:
Name is John
This same approach would work even in a shell without arrays; the column names can just be a space separated list. (Example run in dash, a Posix shell)
$ columns="Name Surname ID Gender"
$ # Here it is vital that $columns not be quoted; we rely on word-splitting
$ while read $columns; do
> echo Name is $Name
> done
John Doe 27 M
Name is John
...
Read the line into an array, then loop through that array and create an associative array that uses the column names.
while read -r line
do
vals=($line)
declare -A colmap
i=0
for col in ${columns[#]}
do
colmap[col]=${vals[$i]}
let i=i+1
done
# do stuff with colmap here
# ...
unset colmap # Clear colmap before next iteration
done < $INPUT

Bash: cant' read out strings with spaces after looping through array of strings

I am using a loop to read the contents of an array, which contains all of the directories and files in the directory hierarchy called 'music' (contents are strings from the previous output of 'find' command). The idea is to separate the full directory path of each array element in "directory_contents" into substrings according to genre, artist, and title. Since my music directory is sorted first by genre, then by artist, then by title, I am grabbing each relevant item using awk where the delimiter "/" shows up. For example, if the directory looks like this after using find "./Electronic/Squarepusher/My Red Hot Car.aif", I will separate "Electronic", "Squarepusher", and "My Red Hot Car", then store them each in separate arrays for genre, artist, and title. Later I will sort these data, then pipe the sorted output into another utility to print all the directory contents in a nice looking table (haven't done this yet). For now, I am just trying to view the results of the string separation with echo statements, and for the most part it seems to work. However, I can't seem to extract substrings which contain spaces, which isn't good:
-->./Hip-Hop/OutKast/title1.aif<--
Genre:
Hip-Hop
Artist:
OutKast
Title:
title1
-->./Hip-Hop/OutKast/title2.aif<--
Genre:
Hip-Hop
Artist:
OutKast
Title:
title2
-->./Hip-Hop/OutKast/title3.aif<--
Genre:
Hip-Hop
Artist:
OutKast
Title:
title3
-->./Jazz/John<--
Genre:
Jazz
Artist:
John
Title:
-->Coltrane/title1.aif<--
Genre:
title1.aif
Artist:
Title:
As you can see, when the loop reads in the string "John Coltrane", it is treating the space as a delimiter, and treating everything after "John" as a different filename. I tried looking for a solution in the bash manual under the section "Arrays" as well as other posts here, but couldn't find a solution that worked for my specific problem (sorry). If anyone has ideas, they would be greatly appreciated. The problematic code appears below, in the for loop (I didn't post the whole script because it is pretty lengthy, but let me if it is needed):
#more before this...
#declare variables
declare -a genre_list
declare -a title_list
declare -a artist_list
declare -a directory_contents
#populate directory with contents
cd $directory
directory_contents=$(find . -mindepth 1 -type f)
cd ..
for music_file in ${directory_contents[*]}; do
if [[ $DEBUG = "true" ]] ; then
echo "-->$music_file<--"
fi
echo "Genre:"
echo $music_file | awk -F"/" '{print $2}'
echo "Artist:"
echo $music_file | awk -F"/" '{print $3}'
echo "Title:"
echo $music_file | awk -F"/" '{print $4}' | awk -F"." '{print $1}'
echo ""
done
Why don't you simply do it in single line:
cd $directory && \
find . -mindepth 3 -maxdepth 3 -type f | \
awk -F'/' '{split($4,A,".aif"); printf "Genre: %s\nArtist: %s\nTitle: %s\n\n",$2,$3,A[1];}'
Update: (removed the .aif from the Title part)
If you can, you should use Perl for this task:
#! /usr/bin/perl
foreach my $fname (<*/*/*>) {
next unless -f $fname;
next unless $fname =~ m"^([^/]+)/([^/]+)/([^/.]+)\.\w+$";
my ($genre, $artist, $title) = ($1, $2, $3);
printf("Genre: %s\nArtist: %s\nTitle: %s\n\n", $genre, $artist, $title);
}
It is faster and simpler than the shell, and it is immune against whitespace in file names.
MUSICDIR="/some/path"
cd "$MUSICDIR"
find . -type f -print | (IFS=/;while read dot genre artist title
do
echo =$genre= =$artist= =$title=
done)
You might try setting the delimiter that bash uses. Perhaps to a newline character?
IFS=\n

Bash: only last iteration of loop index not being stored as new element of array

I have a script that is searching a directory tree for music files, extracting the relevant information from the full path name of music files according to specific attributes (Genre, Artist, Track, Album), storing the formatted substrings as elements in an array, then sorting the elements of the array based on the option which was passed on the command line (e.g. sort by album). Everything seems to be working nicely, except the resulting output of the last iteration of the for loop doesn't seem to be getting stored as a new element in the array. After looking up information on Bash arrays, I found that there is no limitation on the array size. So I am left scratching my head as to why every output of every other iteration up until the last is getting stored in the array. If you look at the output below, you can see that the last element should be the track "Tundra".
(more output above ...)
-->./Hip-Hop/OutKast/Stankonia/Toilet Tisha.aif<--
GENRE:
Hip-Hop
ARTIST:
OutKast
ALBUM:
Stankonia
TITLE:
Toilet Tisha
-->./Electronic/Squarepusher/Hard Normal Daddy/Cooper's World.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Hard Normal Daddy
TITLE:
Cooper's World
-->./Electronic/Squarepusher/Hard Normal Daddy/Papalon.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Hard Normal Daddy
TITLE:
Papalon
-->./Electronic/Squarepusher/Hard Normal Daddy/Vic Acid.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Hard Normal Daddy
TITLE:
Vic Acid
-->./Electronic/Squarepusher/Go Plastic/Go! Spastic.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Go Plastic
TITLE:
Go! Spastic
-->./Electronic/Squarepusher/Go Plastic/Greenways Trajectory.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Go Plastic
TITLE:
Greenways Trajectory
-->./Electronic/Squarepusher/Go Plastic/My Red Hot Car.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Go Plastic
TITLE:
My Red Hot Car
-->./Electronic/Squarepusher/Feed Me Weird Things/Kodack.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Feed Me Weird Things
TITLE:
Kodack
-->./Electronic/Squarepusher/Feed Me Weird Things/North Circular.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Feed Me Weird Things
TITLE:
North Circular
-->./Electronic/Squarepusher/Feed Me Weird Things/Tundra.aif<--
GENRE:
Electronic
ARTIST:
Squarepusher
ALBUM:
Feed Me Weird Things
TITLE:
Tundra
As you can see, the last iteration in the DEBUG section should be the title "Tundra". However, when I display the contents for array "track_list" every track is printed in the desired format except for "Tundra" (look at attached .png file). Does anyone have any idea as to why this might be? Here is a portion of my script:
#more code above ...
#create arrays
declare -a track_list
declare -a directory_contents
#populate directory with files
cd $directory
directory_contents=$(find . -mindepth 1 -type f)
cd ..
IFS=$'\n'
#store each file of directory in track_list
for music_file in ${directory_contents[*]}; do
if [ -n "$DEBUG" ] ; then echo "-->$music_file<--"; fi
this_genre=$(echo $music_file | awk -F"/" '{print $2}')
this_artist=$(echo $music_file | awk -F"/" '{print $3}')
this_album=$(echo $music_file | awk -F"/" '{print $4}')
this_title=$(echo $music_file | awk -F"/" '{print $5}' |\
awk -F".aif" '{print $1}' || awk -F".mp3" '{print $1}' ||\
awk -F".wav" '{print $1}' || awk -F".flac" '{print $1}' \
|| awk -F".m4a" '{print $1}')
function artist_list
{
track=$(printf "%-20s\t\t%-30s\t\t%-30s\t\t%-10s\n"\
"$this_artist" "$this_title" "$this_album" "$this_genre")
track_list=("${track_list[*]}" $track)
}
if [[ $genre = "true" ]] ; then
track=$(printf "%-10s\t\t%-20s\t\t%-30s\t\t%-30s\n"\
"$this_genre" "$this_artist" "$this_title" "$this_album")
track_list=("${track_list[*]}" $track)
elif [[ $artist = "true" ]] ; then
artist_list
elif [[ $album = "true" ]] ; then
track=$(printf "%-30s\t\t%-20s\t\t%-30s\t\t%-10s\n"\
"$this_album" "$this_artist" "$this_title" "$this_genre")
track_list=("${track_list[*]}" $track)
elif [[ $title = "true" ]] ; then
track=$(printf "%-30s\t\t%-20s\t\t%-30s\t\t%-10s\n"\
"$this_title" "$this_artist" "$this_album" "$this_genre")
track_list=("${track_list[*]}" $track)
else
artist_list
fi
if [ -n "$DEBUG" ]; then
echo "GENRE:"
echo $this_genre
echo "ARTIST:"
echo $this_artist
echo "ALBUM:"
echo $this_album
echo "TITLE:"
echo $this_title
echo ""
fi
done
unset IFS
if [[ $genre = "true" ]] ; then
./mulib g
elif [[ $artist = "true" ]] ; then
./mulib a
elif [[ $album = "true" ]] ; then
./mulib m
elif [[ $title = "true" ]] ; then
./mulib t
else
./mulib
fi
echo "$track_list" | sort
echo ""
Dylan,
Your homework give me an idea to do something with my music library too, and
I see than you trying do your homework alone, so here is some comments - how to make an musicsort command :)
The main power of shell programming is in his ability pipelining and decomposing the job into small parts, while these small parts can easily play together.
therefore,
It is not a good idea changing the column orders. See for example ls -l. It does not matter by what you want sort the output (e.g. ls -lt = by time, or ls -ltr = by time but reversed) the column orders remain the same. This way, you can easily pipe output from ls to another command without worrying about the column order. And if you really need changing it, here are already tools what can do it effectively.
My 1st suggestion - don't change the output columns order only sort by them.
Second - the header line. Print put only when really want it. (e.g. for normal output), becuse when you later want pipelining output from your brand new musicsort command, headers will cause much problems. So,
my 2nd suggestion - print header only based on command argument.
When we decomposing your problem, we get:
need some command-line argument handling
need set some defaults, if here are no arguments
need find music files in your music directory
need sort them by criteria
need print them - in sorted order
Skipping 1,2 for now.
finding files in your musicdir is easy.
find "$musicdir" -type f -print
will print out all files recusively. ofc, here can be some cover images and txt lyrics so, need filter them, for example with
find "$musicdir" -type f -print | egrep -i '\.(mp3|aif*|m4p|wav|flac)$'
we have all your music files. Nicely delimited with the '/' character and in order
/path/to/musicdir/genre/artist/album/track.suffix
For the output we need remove the /path/to/musicdir/. It is easy. Here is more way, for example sed.
sed "s:^$musicdir/::;s:\.[^/][^/]*$::"
The above command do two things: 1.) removing the $musicdir path from your filelist, and remove any .suffix too. (like .mp3 .flac etc.). The result is:
genre/artist/album/track
Nice string, clearly separated - so sortable. For the sorting we have the sort command. the sort can sort by any field, and is possible tell him what is the field separator.
E.g.
sort -df -t/ -k2,2
will sort the input separated with '/' by second field (artist). For the -df see man sort.
and finally, we need read the already sorted list of files into variables and print out. (here is ofc another way too). For this bash has the read command. And we must tell bash what is its temporary field separator (IFS), so:
IFS=/; read genre artist album track
and because we have more lines on input need do this in cycle, while we have lines on input.
The final script is here:
musicdir="."
FORMAT="%-20s%-35s%-35s%-35s\n"
sortby=2 #for this example - artist
find "$musicdir" -type f -print |\
egrep -i '\.(aif*|mp3|flac|m4a|wav)$' |\
sed "s:^$musicdir/::;s:\.[^/][^/]*$::" |\
sort -t/ -k$sortby,$sortby | (
IFS=/; while read genre artist album track
do
printf "$FORMAT" $genre $artist $album $track
done)
As you see, the whole searching, sorting, printing is done in few lines. (parts 3, 4, 5).
For the final, need make some argument handling. I wrote one, it is not 100% ok, but working.
The final script what can handle some arguments, set up defaults, and do the main functionality can look like the next: (ofc, here is possible do zilion optimizations, for example combine the egrep and the sed into only one sed and etc...)
#!/bin/bash
#argument handling - not 100% correct, but working...
while getopts "hHgaltd:" arg
do
case "$arg" in
g) sortby=1;;
a) sortby=2;;
l) sortby=3;;
t) sortby=4;;
d) musicdir=$OPTARG;;
H) header=y;;
h|?) echo "Usage: $0 [-d music_dir] [-g|-a|-l|-t] [-H]";
echo ' -d music_dir = path to your music directory (default ".")'
echo ' -g|-a|-l|-t = for sorting by Genre/Artist/aLbum/Track (default "-a")'
echo ' -H print header (default no)'
exit 1;;
esac
done
#defaults
sortby=${sortby:=2};
musicdir=${musicdir:=.}
FORMAT="%-20s%-35s%-35s%-35s\n"
#header only if want one
if [[ $header == "y" ]]
then
printf "$FORMAT" genre artist album track
printf -v line '%*s' 125; echo ${line// /-}
fi
#the main part - search, sort, read into variables, print
find "$musicdir" -type f -print |\
egrep -i '\.(aif*|mp3|flac|m4a|wav)$' |\
sed "s:^$musicdir/::;s:\.[^/][^/]*$::" |\
sort -t/ -k$sortby,$sortby | (
IFS=/; while read genre artist album track
do
printf "$FORMAT" $genre $artist $album $track
done)
so for example the
$ musicsort -t -H -d .
will produce the output sorted by tracks, print header and music is in current directory
genre artist album track
-----------------------------------------------------------------------------------------------------------------------------
Electronic Squarepusher Hard Normal Daddy Cooper's World
Electronic Squarepusher Go Plastic Go! Spastic
Electronic Squarepusher Go Plastic Greenways Trajectory
Electronic Squarepusher Feed Me Weird Things Kodack
Electronic Squarepusher Go Plastic My Red Hot Car
Electronic Squarepusher Feed Me Weird Things North Circular
Electronic Squarepusher Hard Normal Daddy Papalon
Hip-Hop OutKast Stankonia Toilet Tisha
Electronic Squarepusher Feed Me Weird Things Tundra
Electronic Squarepusher Hard Normal Daddy Vic Acid
As you see, the 3/4 lines are argument handling and other things. The main part is done in few lines.
If you really need change the columns order, can be easily done, by adding few formatting lines...

Resources