Merging CSV files : Appending instead of merging - bash

So basically i want to merge a couple of CSV files. Im using the following script to do that :
paste -d , *.csv > final.txt
However this has worked for me in the past but this time it doesn't work. It appends the data next to each other as opposed to below each other. For instance two files that contain records in the following format
CreatedAt ID
Mon Jul 07 20:43:47 +0000 2014 4.86249E+17
Mon Jul 07 19:58:29 +0000 2014 4.86238E+17
Mon Jul 07 19:42:33 +0000 2014 4.86234E+17
When merged give
CreatedAt ID CreatedAt ID
Mon Jul 07 20:43:47 +0000 2014 4.86249E+17 Mon Jul 07 18:25:53 +0000 2014 4.86215E+17
Mon Jul 07 19:58:29 +0000 2014 4.86238E+17 Mon Jul 07 17:19:18 +0000 2014 4.86198E+17
Mon Jul 07 19:42:33 +0000 2014 4.86234E+17 Mon Jul 07 15:45:13 +0000 2014 4.86174E+17
Mon Jul 07 15:34:13 +0000 2014 4.86176E+17
Would anyone know what the reason behind this is? Or what i can do to force merge below records?

Assuming that all the csv files have the same format and all start with the same header,
you can write a little script as the following to append all files in only one and to take only one time the header.
#!/bin/bash
OutFileName="X.csv" # Fix the output name
i=0 # Reset a counter
for filename in ./*.csv; do
if [ "$filename" != "$OutFileName" ] ; # Avoid recursion
then
if [[ $i -eq 0 ]] ; then
head -1 "$filename" > "$OutFileName" # Copy header if it is the first file
fi
tail -n +2 "$filename" >> "$OutFileName" # Append from the 2nd line each file
i=$(( $i + 1 )) # Increase the counter
fi
done
Notes:
The head -1 or head -n 1 command print the first line of a file (the head).
The tail -n +2 prints the tail of a file starting from the lines number 2 (+2)
Test [ ... ] is used to exclude the output file from the input list.
The output file is rewritten each time.
The command cat a.csv b.csv > X.csv can be simply used to append a.csv and b csv in a single file (but you copy 2 times the header).
The paste command pastes the files one on a side of the other. If a file has white spaces as lines you can obtain the output that you reported above.
The use of -d , asks to paste command to define fields separated by a comma ,, but this is not the case for the format of the files you reported above.
The cat command instead concatenates files and prints on the standard output, that means it writes one file after the other.
Refer to man head or man tail for the syntax of the single options (some version allows head -1 other instead head -n 1)...

Alternative simple answer, this as combine_csv.sh:
#!/bin/bash
{ head -n 1 $1 && tail -q -n +2 $*; }
can be used like this:
pattern="my*filenames*.csv"
combine_csv.sh ${pattern} > result.csv

Thank you so much #wahwahwah.
I used your script to make nautilus-action, but it work correctly only with this changes:
#!/bin/bash
for last; do true; done
OutFileName=$last/RESULT_`date +"%d-%m-%Y"`.csv # Fix the output name
i=0 # Reset a counter
for filename in "$last/"*".csv"; do
if [ "$filename" != "$OutFileName" ] ; # Avoid recursion
then
if [[ $i -eq 0 ]] ; then
head -1 "$filename" > "$OutFileName" # Copy header if it is the first file
fi
tail -n +2 "$filename" >> "$OutFileName" # Append from the 2nd line each file
i=$(( $i + 1 )) # Increase the counter
fi
done

Related

How can I change command and option about 'date' command in bash?

I want to convert this bash command to shell script.
BASH
Input:
date --date="Wed Aug 25 22:37:44 +0900 2021" +"%s"
Output:
1629898664
SHELL
tmp.sh:
function time(a, b, c, d, e) { return date --date="a b c d +0900 e" +"%s" }
{print time($1, $2, $3, $4, $5}
timeline:
Wed Aug 25 22:37:44 2021
Command:
awk -f tmp.sh timeline
Output:
awk: tmp.sh:1: function cvtTime(w) { return date --date="Thu May 14 23:40:52 +0900 2020" +"%s" }
awk: tmp.sh:1: ^ syntax error
What about timeline file has multiple lines? Like:
Wed Aug 25 22:37:44 2021 JACK
Wed Aug 26 22:37:44 2021 EMILY
Wed Aug 27 22:37:44 2021 SAM
I tried:
#!/bin/bash
while read -r line; do
date --date="${1} ${2} ${3} ${4} +0900 ${5}" +"%s"
done
Want:
1629898664 JACK
1629985064 EMILY
1630071464 SAM
But it doesn't work :(
It seems that you want a shell script that is invoked with five command line parameters:
A weekday (in a three-letter format)
A month (in a three-letter format)
Day-of-month
A time expression (HH:MM:SS)
A year (four digits)
(Note that 1. is redundant, it is implied by 2., 3., and 5.)
Hence a somewhat minimal shell script would look sth. like this:
#!/bin/bash
date --date="${1} ${2} ${3} ${4} +0900 ${5}" +"%s"
Of course, this can be greatly improved, e.g., by adding sanity checks for the passed parameters.
In case you want to store the date information in a file so that you can pass a single filename parameter to the script instead (allowing for multiple such lines), the following variation will do:
#!/bin/bash
while read -a i; do
echo $(date --date="${i[0]} ${i[1]} ${i[2]} ${i[3]} +0900 ${i[4]}" +"%s") ${i[5]}
done < ${1}
Note, however, that this version expects an additional name parameter after the date information in each line.
In any event, no need for awk here.

Is there a way to sort dates in shell but have Jan come as later than Dec?

I have a list of month, date, and timestamps in a file like this:
Jan19 03:05
Jan19 15:05
Jan20 03:05
Jan20 15:05
Jan21 03:05
Jan21 15:06
Jan22 03:05
Jan22 15:06
Dec25 15:05
Dec26 14:06
Dec27 15:06
Dec28 15:06
Dec29 14:05
Dec30 14:06
Dec31 15:06
I need to just get the most recent 30 entries. My code is:
cat file | sort -k1.1,1.3M -k1.4n -k2V
This sort is sorting the Dec entries as more recent than the Jan. I think it's because 12 is bigger than 1 but is there a way to get Jan to come to the end of this file?
Assuming the all dates in the list are within last one year from now, how about:
now=$(date "+%m%d %H:%M") # current date and time
declare -A m2n=([Jan]="01" [Feb]="02" [Mar]="03" [Apr]="04"
[May]="05" [Jun]="06" [Jul]="07" [Aug]="08"
[Sep]="09" [Oct]="10" [Nov]="11" [Dec]="12"
)
while IFS= read -r line; do
if [[ $line =~ (^[A-Z][a-z]{2})(.+) ]]; then
datetime="${m2n[${BASH_REMATCH[1]}]}${BASH_REMATCH[2]}"
if [[ $datetime > $now ]]; then
datetime="0$datetime" # previous year
else
datetime="1$datetime" # this year
fi
printf "%s\t%s\n" "$datetime" "$line"
else
echo "$line" # does not match the expected format
fi
done < file | sort | cut -f 2-
It compares each date string with current date/time in dictionary order.
If the latter is larger, the date is assumed to be this year and put "1"
in the "year" field, else put "0" there.
Then prepend the generated date string in front of the original lines,
sort, and finally remove the portion.
Just replace with regex to prepare the data for sorting with a value for Jan that will come after Dec:
# JanXY -> 13 XY JanXY ; DecXY -> 12 XY DecXY
sed 's/^Jan\([0-9][0-9]\)/13 \1 &/; s/^Dec\([0-9][0-9]\)/12 \1 &/' |
sort -n -k1 -k2 | cut -d' ' -f3-5

Collect info from multiple lines

I need to extract certain info from multiple lines (5 lines every transaction) and make the output as csv file. These lines are coming from a maillog wherein every transaction has its own transaction id. Here's one sample transaction:
Nov 17 00:15:19 server01 sm-mta[14107]: tAGGFJla014107: from=<sender#domain>, size=2447, class=0, nrcpts=1, msgid=<201511161615.tAGGFJla014107#server01>, proto=ESMTP, daemon=MTA, tls_verify=NONE, auth=NONE, relay=[100.24.134.19]
Nov 17 00:15:19 server01 flow-control[6033]: tAGGFJla014107 accepted
Nov 17 00:15:19 server01 MM: [Jilter Processor 21 - Async Jilter Worker 9 - 127.0.0.1:51698-tAGGFJla014107] INFO user.log - virus.McAfee: CLEAN - Declaration for Shared Parental Leave Allocation System
Nov 17 00:15:19 server01 MM: [Jilter Processor 21 - Async Jilter Worker 9 - 127.0.0.1:51698-tAGGFJla014107] INFO user.log - mtaqid=tAGGFJla014107, msgid=<201511161615.tAGGFJla014107#server01>, from=<sender#domain>, size=2488, to=<recipient#domain>, relay=[100.24.134.19], disposition=Deliver
Nov 17 00:15:20 server01 sm-mta[14240]: tAGGFJla014107: to=<recipient#domain>, delay=00:00:01, xdelay=00:00:01, mailer=smtp, pri=122447, relay=relayserver.domain. [100.91.20.1], dsn=2.0.0, stat=Sent (tAGGFJlR021747 Message accepted for delivery)
What I tried is, I made these 5 lines into 1 line and used awk to parse each column - unfortunately, the column count is not uniform.
I'm looking into getting the date/time (line 1, columns 1-3), sender, recipient, and subject (line 3, words after "CLEAN -" to the end of line)
Preferably sed or awk in bash.
Thanks!
Explanation: fileis your file.
The script initializes id and block to empty strings. At first run id takes the value of field nr. 7. After that all lines are added to block until a line doesn't match id. At that point block and id are reinitialized.
awk 'BEGIN{id="";block=""} {if (id=="") id=$6; else {if ($0~id) block= block $0; else {print block;block=$0;id=$6}}}' file
Then you're going to have to process each line of the output.
There are many ways to approach this. Here is one example calling a simple script and passing the log filename as the first argument. It will parse the requested data and save the data separated into individual variables. It simply prints the results at the end.
#!/bin/bash
[ -r "$1" ] || { ## validate input file readable
printf "error: invalid argument, file not readable '%s'\n" "$1"
exit 1
}
while read -r line; do
## set date from line containing from/sender
if grep -q -o 'from=<' <<<"$line" &>/dev/null; then
dt=$(cut -c -15 <<<"$line")
from=$(grep -o 'from=<[a-zA-Z0-9]*#[a-zA-Z0-9]*>' <<<"$line")
sender=${from##*<}
sender=${sender%>*}
fi
## search each line for CLEAN
if grep -q -o 'CLEAN.*$' <<<"$line" &>/dev/null; then
subject=$(grep -o 'CLEAN.*$' <<<"$line")
subject="${subject#*CLEAN - }"
fi
## search line for to
if grep -q -o 'to=<' <<<"$line" &>/dev/null; then
to=$(grep -o 'to=<[a-zA-Z0-9]*#[a-zA-Z0-9]*>' <<<"$line")
to=${to##*<}
to=${to%>*}
fi
done < "$1"
printf " date : %s\n from : %s\n to : %s\n subject: \"%s\"\n" \
"$dt" "$sender" "$to" "$subject"
Input
$ cat dat/mail.log
Nov 17 00:15:19 server01 sm-mta[14107]: tAGGFJla014107: from=<sender#domain>, size=2447, class=0, nrcpts=1, msgid=<201511161615.tAGGFJla014107#server01>, proto=ESMTP, daemon=MTA, tls_verify=NONE, auth=NONE, relay=[100.24.134.19]
Nov 17 00:15:19 server01 flow-control[6033]: tAGGFJla014107 accepted
Nov 17 00:15:19 server01 MM: [Jilter Processor 21 - Async Jilter Worker 9 - 127.0.0.1:51698-tAGGFJla014107] INFO user.log - virus.McAfee: CLEAN - Declaration for Shared Parental Leave Allocation System
Nov 17 00:15:19 server01 MM: [Jilter Processor 21 - Async Jilter Worker 9 - 127.0.0.1:51698-tAGGFJla014107] INFO user.log - mtaqid=tAGGFJla014107, msgid=<201511161615.tAGGFJla014107#server01>, from=<sender#domain>, size=2488, to=<recipient#domain>, relay=[100.24.134.19], disposition=Deliver
Nov 17 00:15:20 server01 sm-mta[14240]: tAGGFJla014107: to=<recipient#domain>, delay=00:00:01, xdelay=00:00:01, mailer=smtp, pri=122447, relay=relayserver.domain. [100.91.20.1], dsn=2.0.0, stat=Sent (tAGGFJlR021747 Message accepted for delivery)
Output
$ bash parsemail.sh dat/mail.log
date : Nov 17 00:15:19
from : sender#domain
to : recipient#domain
subject: "Declaration for Shared Parental Leave Allocation System"
Note: if your from/sender is not always going to be in the first line, you can simply move those lines out from under the test clause. Let me know if you have any questions.

Bash script assistance with renaming file using existing parts of filename

I'm looking for help with a bash script to do some renaming of files for me. I don't know much about bash scripting, and what I have read is overwhelming. It's a lot to know/understand for the limited applications I will probably have.
In Dropbox, my media files are named something like:
Photo Jul 04, 5 49 44 PM.jpg
Video Jun 22, 11 21 00 AM.mov
I'd like them to be renamed in the following format: 2015-07-04 1749.ext
Some difficulties:
The script has to determine if AM or PM to put in the correct 24-hour format
The year is not specified; it is safe to assume the current year
The date, minute and second have a leading zero, but the hour does not; therefore the position after the hour is not absolute
Any assistance would be appreciated. FWIW, I'm running MacOS.
Mac OSX
This uses awk to reformat the date string:
for f in *.*
do
new=$(echo "$f" | awk -F'[ .]' '
BEGIN {
split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec",month)
for (i in month) {
nums[month[i]]=i
}
}
$(NF-1)=="PM" {$4+=12;}
{printf "%s 2015-%02i-%02i %02i%02i.%s",$1,nums[$2],$3,$4,$5,$8;}
')
mv "$f" "$new"
done
After the above was run, the files are now named:
$ ls -1 *.*
Photo 2015-07-04 1749.jpg
Video 2015-06-22 1121.mov
The above was tested on GNU awk but I don't believe that I have used any GNU-specific features.
GNU/Linux
GNU date has a handy feature for interpreting human-style date strings:
for f in *.*
do
prefix=${f%% *}
ext=${f##*.}
datestr=$(date -d "$(echo "$f" | sed 's/[^ ]* //; s/[.].*//; s/ /:/3; s/ /:/3; s/,//')" '+%F %H%M')
mv "$f" "$prefix $datestr.$ext"
done
Here is an example of the script in operation:
$ ls -1 *.*
Photo Jul 04, 5 49 44 PM.jpg
Video Jun 22, 11 21 00 AM.mov
$ bash script
$ ls -1 *.*
Photo 2015-07-04 1749.jpg
Video 2015-06-22 1121.mov
While not a simple parse and reformat for date, it isn't that difficult. The bash string tools of parameter expansion/substring removal are all you need to parse the pieces of the date into a format that date can use to output a new date string in the format for use in a filename. (see String Manipulation ) date -d is used to generate a new date string based on the contents of the original filename.
Note: the following presumes the dropbox filenames are in the format you have specified. (it doesn't care what the first part of the name or extension is as long as it matches the format you have specified) Here is an example of properly isolating the pieces of the filename needed to generate a date in the format specified)
Further, all spaces have been removed from the filename. While you originally showed a space between the day and hours, I will not provide an example of poor practice by inserting a space in a filename. As such, the spaces have been replaced with '_' and '-':
#!/bin/bash
# Photo Jul 04, 5 49 44 PM.jpg
# Video Jun 22, 11 21 00 AM.mov
# fn="Photo Jul 04, 5 49 44 PM.jpg"
fn="Video Jun 22, 11 21 00 AM.mov"
ext=${fn##*.} # determine extension
prefix=${fn%% *} # determine prefix (Photo or Video)
datestr=${fn%.${ext}} # remove extension from filename
datestr=${datestr#${prefix} } # remove prefix from datestr
day=${datestr%%,*} # isolate Month and date in day
ampm=${datestr##* } # isloate AM/PM in ampm
datestr=${datestr% ${ampm}} # remove ampm from datestr
timestr=${datestr##*, } # isolate time in timestr
timestr=$(tr ' ' ':' <<<"$timestr") # translate spaces to ':' using herestring
cmb="$day $timestr $hr" # create combined date/proper format
## create date/time string for filename
datetm=$(date -d "$cmb" '+%Y%m%d-%H%M')
newfn="${prefix}_${datetm}.${ext}"
## example moving of file to new name
# (assumes you handle the path correctly)
printf "mv '%s' %s\n" "$fn" "$newfn"
# mv "$fn" "$newfn" # uncomemnt to actually use
exit 0
Example/Output
$ bash dateinfname.sh
mv 'Video Jun 22, 11 21 00 AM.mov' Video_20150622-1121.mov

How to store directory files listing into an array?

I'm trying to store the files listing into an array and then loop through the array again.
Below is what I get when I run ls -ls command from the console.
total 40
36 -rwxrwxr-x 1 amit amit 36720 2012-03-31 12:19 1.txt
4 -rwxrwxr-x 1 amit amit 1318 2012-03-31 14:49 2.txt
The following bash script I've written to store the above data into a bash array.
i=0
ls -ls | while read line
do
array[ $i ]="$line"
(( i++ ))
done
But when I echo $array, I get nothing!
FYI, I run the script this way: ./bashscript.sh
I'd use
files=(*)
And then if you need data about the file, such as size, use the stat command on each file.
Try with:
#! /bin/bash
i=0
while read line
do
array[ $i ]="$line"
(( i++ ))
done < <(ls -ls)
echo ${array[1]}
In your version, the while runs in a subshell, the environment variables you modify in the loop are not visible outside it.
(Do keep in mind that parsing the output of ls is generally not a good idea at all.)
Here's a variant that lets you use a regex pattern for initial filtering, change the regex to be get the filtering you desire.
files=($(find -E . -type f -regex "^.*$"))
for item in ${files[*]}
do
printf " %s\n" $item
done
This might work for you:
OIFS=$IFS; IFS=$'\n'; array=($(ls -ls)); IFS=$OIFS; echo "${array[1]}"
Running any shell command inside $(...) will help to store the output in a variable. So using that we can convert the files to array with IFS.
IFS=' ' read -r -a array <<< $(ls /path/to/dir)
You may be tempted to use (*) but what if a directory contains the * character? It's very difficult to handle special characters in filenames correctly.
You can use ls -ls. However, it fails to handle newline characters.
# Store la -ls as an array
readarray -t files <<< $(ls -ls)
for (( i=1; i<${#files[#]}; i++ ))
{
# Convert current line to an array
line=(${files[$i]})
# Get the filename, joining it together any spaces
fileName=${line[#]:9}
echo $fileName
}
If all you want is the file name, then just use ls:
for fileName in $(ls); do
echo $fileName
done
See this article or this this post for more information about some of the difficulties of dealing with special characters in file names.
My two cents
The asker wanted to parse output of ls -ls
Below is what I get when I run ls -ls command from the console.
total 40
36 -rwxrwxr-x 1 amit amit 36720 2012-03-31 12:19 1.txt
4 -rwxrwxr-x 1 amit amit 1318 2012-03-31 14:49 2.txt
But there are few answer addressing this parsing operation.
ls's output
Before trying to parse something, we have to ensure command output is consistant, stable and easy to parse as possible
In order to ensure output wont be altered by some alias you may prefer to specify full path of command: /bin/ls.
Avoid variations of output due to locales, prefix your command by LANG=C LC_ALL=C
Use --time-style command switch to use UNIX EPOCH more easier to parse time infos.
Use -b switch for holding special characters
So we will prefer
LANG=C LC_ALL=C /bin/ls -lsb --time-style='+%s.%N'
to just
ls -ls
Full bash sample
#!/bin/bash
declare -a bydate=() bysize=() byname=() details=()
declare -i cnt=0 vtotblk=0 totblk
{
read -r _ totblk # ignore 1st line
while read -r blk perm lnk usr grp sze date file;do
byname[cnt]="${file//\\ / }"
details[cnt]="$blk $perm $lnk $usr $grp $sze $date"
bysize[sze]+="$cnt "
bydate[${date/.}]+="$cnt "
cnt+=1 vtotblk+=blk
done
} < <(LANG=C LC_ALL=C /bin/ls -lsb --time-style='+%s.%N')
From there, you could easily sort by dates, sizes of names (sorted by ls command).
echo "Path '$PWD': Total: $vtotblk, sorted by dates"
for dte in ${!bydate[#]};do
printf -v msec %.3f .${dte: -9}
for idx in ${bydate[dte]};do
read -r blk perm lnk usr grp sze date <<<"${details[idx]}"
printf ' %11d %(%a %d %b %T)T%s %s\n' \
$sze "${date%.*}" ${msec#0} "${byname[idx]}"
done
done
echo "Path '$PWD': Total: $vtotblk, sorted by sizes"
for sze in ${!bysize[#]};do
for idx in ${bysize[sze]};do
read -r blk perm lnk usr grp sze date <<<"${details[idx]}"
printf -v msec %.3f .${date#*.}
printf ' %11d %(%a %d %b %T)T%s %s\n' \
$sze "${date%.*}" ${msec#0} "${byname[idx]}"
done
done
echo "Path '$PWD': Total: $vtotblk, sorted by names"
for((idx=0;idx<cnt;idx++));{
read -r blk perm lnk usr grp sze date <<<"${details[idx]}"
printf -v msec %.3f .${date#*.}
printf ' %11d %(%a %d %b %T)T%s %s\n' \
$sze "${date%.*}" ${msec#0} "${byname[idx]}"
}
( Accessory, you could check if total block printed by ls match total block by lines:
(( vtotblk == totblk )) ||
echo "WARN: Total blocks: $totblk != Block count: $vtotblk" >&2
Of course, this could be inserted before first echo "Path...;)
Here is an output sample. (Note: there is a filename with a newline)
Path '/tmp/so': Total: 16, sorted by dates
0 Sun 04 Sep 10:09:18.221 2.txt
247 Mon 05 Sep 09:11:50.322 Filename with\nsp\303\251cials characters
13 Mon 05 Sep 10:12:24.859 1.txt
1313 Mon 05 Sep 11:01:00.855 parseLs.00
1913 Thu 08 Sep 08:20:20.836 parseLs
Path '/tmp/so': Total: 16, sorted by sizes
0 Sun 04 Sep 10:09:18.221 2.txt
13 Mon 05 Sep 10:12:24.859 1.txt
247 Mon 05 Sep 09:11:50.322 Filename with\nsp\303\251cials characters
1313 Mon 05 Sep 11:01:00.855 parseLs.00
1913 Thu 08 Sep 08:20:20.836 parseLs
Path '/tmp/so': Total: 16, sorted by names
13 Mon 05 Sep 10:12:24.859 1.txt
0 Sun 04 Sep 10:09:18.221 2.txt
247 Mon 05 Sep 09:11:50.322 Filename with\nsp\303\251cials characters
1913 Thu 08 Sep 08:20:20.836 parseLs
1313 Mon 05 Sep 11:01:00.855 parseLs.00
And if you want to format characters (with care: there could be some issues, if you don't know who create content of path). But if folder is your, you could:
echo "Path '$PWD': Total: $vtotblk, sorted by dates, with special chars"
printf -v spaces '%*s' 37 ''
for dte in ${!bydate[#]};do
printf -v msec %.3f .${dte: -9}
for idx in ${bydate[dte]};do
read -r blk perm lnk usr grp sze date <<<"${details[idx]}"
printf ' %11d %(%a %d %b %T)T%s %b\n' $sze \
"${date%.*}" ${msec#0} "${byname[idx]//\\n/\\n$spaces}"
done
done
Could output:
Path '/tmp/so': Total: 16, sorted by dates, with special chars
0 Sun 04 Sep 10:09:18.221 2.txt
247 Mon 05 Sep 09:11:50.322 Filename with
spécials characters
13 Mon 05 Sep 10:12:24.859 1.txt
1313 Mon 05 Sep 11:01:00.855 parseLs.00
1913 Thu 08 Sep 08:20:20.836 parseLs
Isn't these 2 code lines, either using scandir or including the dir pull in the declaration line, supposed to work?
src_dir="/3T/data/MySQL";
# src_ray=scandir($src_dir);
declare -a src_ray ${src_dir/*.sql}
printf ( $src_ray );
In the conversation over at https://stackoverflow.com/a/9954738/11944425
the behavior can be wrapped into a convenience function which applies some action to entries of the directory as string values.
#!/bin/bash
iterfiles() {
i=0
while read filename
do
files[ $i ]="$filename"
(( i++ ))
done < <( ls -l )
for (( idx=0 ; idx<${#files[#]} ; idx++ ))
do
$# "${files[$idx]}" &
wait $!
done
}
where $# is the complete glob of arguments passed to the function! This lets the function have the utility to take an arbitrary command as a partial function of sorts to operate on the filename:
iterfiles head -n 1 | tee -a header_check.out
When a script needs to iterate over files, returning an array of them is not possible. The workaround is to define the array outside of the function scope (and possibly unset it later) — modifying it inside the function's scope. Then, after the function is called by a script, the array variable becomes available. For instance, the mutation on files demonstrates how this could be done.
declare -a files # or just `files= ` (nothing)
iterfiles() {
# ...
files=...
}
Extending the conversation above, #Jean-BaptistePoittevin pointed out a valuable detail.
#!/bin/bash
# Adding a section to unset certain variable names that
# may already be active in the shell.
unset i
unset files
unset omit
i=0
omit='^([\n]+)$'
while read file
do
files[ $i ]="$file"
(( i++ ))
done < <(ls -l | grep -Pov ${omit} )
Note: This can be tested using echo ${files[0]} or for entry in ${files[#]}; do ... ; done
Often times, the circumstance could require an absolute path in double quotes, where the file (or ancestor directories) have spaces or unusual characters in the name. find is one answer here. The simplest usage might look like the above one, except done < <(ls -l ... ) is replaced with:
done < <(find /path/to/directory ! -path /path/to/directory -type d)
Its convenient when you need absolute paths in double quotes as an iterable collection to use a recipe like the one below. When export is not used, the shell does not update the environment namespace to include it in the find subshell:
#!/bin/bash
export DIRECTORY="$PWD" # For example
declare -a files
i=0
while read filename; do
files[ $i ]="$filename"
done < <(find $DIRECTORY ! -path $DIRECTORY -type d)
for (( idx=0; idx<${#files[#]}; idx++ )); do
# Make a templated string for macro script generation
quoted_path="\"${files[$idx]}\""
if [[ "$(echo $quoted_path | grep some_substring | wc -c)" != "0" ]]; then
echo "mv $quoted_path /some/other/watched/folder/" >> run_nightly.sh
fi
done
Upon running this, ./run_nightly.sh will be populated with bulk commands to move a quoted path to /some/other/watched/folder/. This kind of scripting pattern will make it possible to supercharge your scripts.
simply you can use this below for loop (do not forget to quote to handle filenames with spaces)
declare -a arr
arr=()
for file in "*.txt"
do
arr=(${arr[*]} "$file")
done
Run
for file in ${arr[*]}
do
echo "<$file>"
done
to test.

Resources