Match first few letters of a file name : Shell script - bash

I am trying to match first few letters of a file.
for entry in `ls`; do
echo $entry
done
With the above code I get the name of all the files.
I have a few files with similar name at the start:
Beaglebone-v1
Beaglebone-v3
Beaglebone-v2
How can I compare $entry with Beaglebone* and then extract the latest version file name?

If you want to loop over all Beaglebone-* files:
for entry in Beaglebone-* ; do
echo $entry
done
if you just need the file with the latest version, you can depend on the fact that ls sorts your names alphabetically, so you could just do:
LATEST_FILE_NAME=$(ls Beaglebone-* | tail -n 1)
which will just take the last one alphabetically.

To deal with larger numbers, you could use numeric comparison like this:
stem="Beaglebone-v"
for file in $stem*; do
ver=${file#"$stem"} # cut away stem to get version number
(( ver > max )) && max=$ver # conditionally assign `ver` to `max`
done
echo "$stem$max"
Testing it out:
bash-4.3$ ls Beaglebone-v*
Beaglebone-v1 Beaglebone-v10 Beaglebone-v2 Beaglebone-v3
bash-4.3$ stem="Beaglebone-v" &&
for file in $stem*
do
ver=${file#"$stem"}
(( ver > max )) && max=$ver
done; echo "$stem$max"
Beaglebone-v10

You can store the filenames matching the pattern in an array and then pick the last element of the array.
shopt -s nullglob
arr=( Beaglebone-* )
if (( ${#arr[#]} > 0 ))
then
latest="${arr[ (( ${#arr[#]} - 1 )) ]}"
echo "$latest"
fi
You need to enable nullglob so that if there are no files matching the pattern, you will get an empty array rather than the pattern itself.

If version numbers can go beyond single digits,
function version_numbers {
typeset w; for w in $1-v*; do echo ${w#$1-v}; done
}
version_numbers "Beaglebone" | sort -n | tail -1
Or, adding function max:
# read a stream of numbers, from stdin (one per line)
# and return the largest value
function max
{
typeset _max n
read _max || return
while read n; do
((_max < n)) && _max=$n
done
echo $_max
}
We can now do the whole thing without external commands:
version_numbers Beaglebone | max
Note that max will fail horribly if any one line fails the numerical comparison.

Related

Creating files in succession

How would one go about creating a script for creating 25 empty files in succession? (I.e 1-25, 26-51, 52-77)
I can create files 1-25 but I’m having trouble figuring out how to create a script that continues that process from where it left off, every time I run the script.
#!/bin/bash
higher=$( find files -type f -exec basename {} \; | sort -n | tail -1 )
if [[ "$higher" == "" ]]
then
start=1
end=25
else
(( start = higher + 1 ))
(( end = start + 25 ))
fi
echo "$start --> $end"
for i in $(seq $start 1 $end)
do
touch files/"$i"
done
I put my files in a directory called "files".
hence the find on directory "files".
for each file found, I run a basename on it. That will return only integer values, since the files all have a number filename.
sort -n puts them in order.
tail -1 extracts the highest number.
if there are no files, higher will be empty, so the indexes will be 1 and 25.
otherwise, they will be higher + 1, and higher + 26.
I used seq for the for loop to avoid problems with variables inside a range definition (you did {1..25})
#! /usr/bin/env bash
declare -r base="${1:-base-%d.txt}"
declare -r lot="${2:-25}"
declare -i idx=1
declare -i n=0
printf -v filename "${base}" ${idx}
while [[ -e "${filename}" ]]; do
idx+=1
printf -v filename "${base}" "${idx}"
done
while [[ $n -lt $lot ]]; do
printf -v filename "${base}" ${idx}
if [[ ! -e "${filename}" ]]; then
> "$filename"
n+=1
fi
idx+=1
done
This script accepts two optional parameters.
The first is the basename of your future files with a %d token automatically replaced by the file number. Default value is base-%d.txt;
The number of file to create. Default value is 25.
How script works:
Variable declarations
base: file basename (constant)
lot: number of file to create (constant)
idx: search index
n: counter for new files
Search files already created from 1
The loop stop at first hole in the numbering
Loop to create empty files
The condition in the loop allows to fill in the numbering holes
> filename create an empty file

Process files in pairs

I have a list of files:
file_name_FOO31101.txt
file_name_FOO31102.txt
file_name_FOO31103.txt
file_name_FOO31104.txt
And I want to use pairs of files for input into a downstream program such as:
program_call file_name_01.txt file_name_02.txt
program_call file_name_03.txt file_name_04.txt
...
I do not want:
program_call file_name_02.txt file_name_03.txt
I need to do this in a loop as follows:
#!/bin/bash
FILES=path/to/files
for file in $FILES/*.txt;
do
stem=$( basename "${file}" ) # stem : file_name_FOO31104_info.txt
output_base=$( echo $stem | cut -d'_' -f 1,2,3 ) # output_base : FOO31104_info.txt
id=$( echo $stem | cut -d'_' -f 3 ) # get the first field : FOO31104
number=$( echo -n $id | tail -c 2 ) # get the last two digits : 04
echo $id $((id+1))
done
But this does not produce what I want.
In each loop I want to call a program once, with two files as input (last 2 digits of first file always odd 01, last 2 digits of second file always even 02)
I actually wouldn't use a for loop at all. A while loop that shifts files off is a perfectly reasonable way to do this.
# here, we're overriding the argument list with the list of files
# ...you can do this in a function if you want to keep the global argument list intact
set -- "$FILES"/*.txt ## without these quotes paths with spaces break
# handle the case where no files were found matching our glob
[[ -e $1 || -L $1 ]] || { echo "No .txt found in $FILES" >&2; exit 1; }
# here, we're doing our own loop over those arguments
while (( "$#" > 1 )); do ## continue in the loop only w/ 2-or-more remaining
echo "Processing files $1 and $2" ## ...substitute your own logic here...
shift 2 || break ## break even if test doesn't handle this case
done
# ...and add your own handling for the case where there's an odd number of files.
(( "$#" )) && echo "Left over file $1 still exists"
Note that the $#s are quoted inside (( )) here for StackOverflow's syntax highlighting, not because they otherwise need to be. :)
By the way -- consider using bash's native string manipulation.
stem=${file##*/}
IFS=_ read -r p1 p2 id p_rest <<<"$stem"
number=${id:$(( ${#id} - 2 ))}
output_base="${p1}${p2}${id}"
echo "$id $((10#number + 1))" # 10# ensures interpretation as decimal, not octal

Bash - Stripping and adding leading zeros to numbers before concatenating into string ordered strings

I need to automate a backup solution which stores files in folders such as YYYYMMDD.nn.
Every day few files would be backed up like this so the resulting folder names could be 20141002.01, 20141002.2 ... 20141002.10. My current script works for YYYYMMDD.n but when n is more than 9 sorting and picking up the last folder doesn't work because 20141002.10 is above 20141002.9 hens switching to YYYYMMDD.nn format and the approach of separating the nn, stripping leading zeros, then incrementing, and adding leading zeros if needed.
I have a function which checks the last folder for today's date and creates the next one.
createNextProcessedFolder() {
local LastFolderName=`ls -1 ${ProcessedListsDir} | grep ${CurrentDate} | tail -n 1`
n=`echo ${LastFolderName} | sed -r 's/^.{9}//'`
n="$((10#$n))"
nextFolderName=${CurrentDate}.$((if[[ $(( ${n}+1 )) < 10 ]];then n="0$((${n}+1))";else n="$(( ${n}+1 ))"; fi))
mkdir ${ProcessedListsDir}/${nextFolderName}
if [[ -d ${ProcessedListsDir}/${nextFolderName} ]]
then
echo "New folder ${nextFolderName} was created"
else
echo "Error: ${nextFolderName} was not created"
fi
Location="${ProcessedListsDir}/${nextFolderName}"
}
So when I try to run this I get an error like:
line 21: if[[ 1 < 10 ]];then n="01";else n="1"; fi: syntax error: invalid arithmetic operator (error token is ";then n="01";else n="1"; fi")
Line 21 is:
nextFolderName=${CurrentDate}.$((if[[ $(( ${n}+1 )) < 10 ]];then n="0$((${n}+1))";else n="$(( ${n}+1 ))"; fi))
I'm sure there will be more errors after this one but I would really appreciate if somebody helped me with this.
You cannot use $((...)) for command substitution as it needs to be $(...)
You need spaces before and after [[ and ]]. You can also use ((...)) in BASH:
Try this:
(( (n+1) < 10 )) && n="0$((n++))" || ((n++))
nextFolderName="${CurrentDate}.${n}"
For completeness, another solution is:
n=$( printf "%02d" $n )
The 02 before the d means prepend with 0s up to 2 digits. Or:
nextFolderName="${CurrentDate}."$( printf "%02d" "$n" )
So my problem was with incrementing a number witch was extracted from a string with a leading zero and then returning the incremented number with a leading zero if smaller than 10. The solution I ended up using can be represented with the below script.
I guess it can't be shorter than that
n=$1
(( ((n++)) < 10 )) && n="0$n"
echo $n
Something I didn't expect is that I don't have to strip leading zeros from n using this, n++ does it while incrementing :-)
Thanks again anubhava for pointing me in the right direction.

Creating a bash script that acts as a fortune teller or "magic 8 ball" essentially

I am trying to create a bash script that is essentially like a magic 8 ball with 6 different responses (Yes, No, Maybe, Hard to tell, Unlikely, and Unknown). The key is that once a response is given, it should not be given again until all responses have been given.
Here is what I have so far:
#!/bin/bash
echo "Ask and you shall receive your fortune: "
n=$((RANDOM*6/32767))
while [`grep $n temp | wc awk '{print$3}'` -eq 0]; do
n=$((RANDOM*6/32767))
done
grep -v $n temp > temp2
mv temp2 temp
Basically I have the 6 responses all on different lines in the temp file, and I am trying to construct the loops so that once a response is given, it creates a new file without that response (temp2), then copies it back to temp. Then once the temp file is empty it will continue from the beginning.
I'm quite positive that my current inner loop is wrong, and that I need an outer loop, but I'm fairly new to this and I am stuck.
Any help will be greatly appreciated.
Try something like this:
#!/bin/bash
shuffle() {
local i tmp size max rand
# $RANDOM % (i+1) is biased because of the limited range of $RANDOM
# Compensate by using a range which is a multiple of the array size.
size=${#array[*]}
max=$(( 32768 / size * size ))
for ((i=size-1; i>0; i--)); do
while (( (rand=$RANDOM) >= max )); do :; done
rand=$(( rand % (i+1) ))
tmp=${array[i]} array[i]=${array[rand]} array[rand]=$tmp
done
}
array=( 'Yes' 'No' 'Maybe' 'Hard to tell' 'Unknown' 'Unlikely' )
shuffle
for var in "${array[#]}"
do
echo -n "Ask a question: "
read q
echo "${var}"
done
I wrote a script that follows your initial approach (using temp files):
#!/bin/bash
# Make a copy of temp, so you don't have to recreate the file every time you run this script
TEMP_FILE=$(tempfile)
cp temp $TEMP_FILE
# You know this from start, the file contains 6 possible answers, if need to add more in future, change this for the line count of the file
TOTAL_LINES=6
echo "Ask and you shall receive your fortune: "
# Dummy reading of the char, adds a pause to the script and involves the user interaction
read
# Conversely to what you stated, you don't need an extra loop, with one is enough
# just change the condition to count the line number of the TEMP file
while [ $TOTAL_LINES -gt 0 ]; do
# You need to add 1 so the answer ranges from 1 to 6 instead of 0 to 5
N=$((RANDOM*$TOTAL_LINES/32767 + 1))
# This prints the answer (grab the first N lines with head then remove anything above the Nth line with tail)
head -n $N < $TEMP_FILE | tail -n 1
# Get a new file deleting the $N line and store it in a temp2 file
TEMP_FILE_2=$(tempfile)
head -n $(( $N - 1 )) < $TEMP_FILE > $TEMP_FILE_2
tail -n $(( $TOTAL_LINES - $N )) < $TEMP_FILE >> $TEMP_FILE_2
mv $TEMP_FILE_2 $TEMP_FILE
echo "Ask and you shall receive your fortune: "
read
# Get the total lines of TEMP (use cut to delete the file name from the wc output, you only need the number)
TOTAL_LINES=$(wc -l $TEMP_FILE | cut -d" " -f1)
done
$ man shuf
SHUF(1) User Commands
NAME
shuf - generate random permutations
SYNOPSIS
shuf [OPTION]... [FILE]
shuf -e [OPTION]... [ARG]...
shuf -i LO-HI [OPTION]...
DESCRIPTION
Write a random permutation of the input lines to standard output.
More stuff follows, you can read it on your own machine :)

How to know if file in a loop is the last one?

Example
for FILE in $DIR/*
do
if(<is last File>)
doSomethingSpecial($FILE)
else
doSomethingRegular($FILE)
fi
done
What to call for <is last file> to check if the current file is the last one in the array ?
Is there an easy built-in check without checking the array's length by myself ?
What to call for to check if the current file is the last one in the array ?
For a start, you are not using an array. If you were then it would be easy:
declare -a files
files=($DIR/*)
pos=$(( ${#files[*]} - 1 ))
last=${files[$pos]}
for FILE in "${files[#]}"
do
if [[ $FILE == $last ]]
then
echo "$FILE is the last"
break
else
echo "$FILE"
fi
done
I know of no way to tell that you are processing the last element of a list in a for loop. However you could use an array, iterate over all but the last element, and then process the last element outside the loop:
files=($DIR/*)
for file in "${files[#]::${#files[#]}-1}" ; do
doSomethingRegular "$file"
done
doSomethingSpecial "${files[#]: -1:1}"
The expansion ${files[#]:offset:length} evaluates to all the elements starting at offset (or the beginning if empty) for length elements. ${#files[#]}-1 is the number of elements in the array minus 1.
${files[#]: -1:1} evaluates to the last element - -1 from the end, length 1. The space is necessary as :- is treated differently to : -.
Try this
LAST_FILE=""
for f in *
do
if [ ! -z $LAST_FILE ]
then
echo "Process file normally $LAST_FILE"
fi
LAST_FILE=$f
done
if [ ! -z $LAST_FILE ]
then
echo "Process file as last file $LAST_FILE"
fi
Produces
bash[1051]: ls
1 2 3 4
bash[1052]: sh ../last_file.sh
Process file normally 1
Process file normally 2
Process file normally 3
Process file as last file 4
You can use find to find the total number of files.
Then when you are in the loop count to the total number and carry out your task when the total equals the count i.e, the last file.
f=0
tot_files=`find . -iname '*.txt' | wc -l`
for FILE in $DIR/*
do
f=($f+1)
if [[ $f == $tot_files ]];then
carryout your task
fi
done
Building on the current highest-voted answer from #cdarke (https://stackoverflow.com/a/12298757/415523), if looking at a general array of values (rather than specifically files on disk), the loop code would be as follows:
declare -a array
declare -i length current
array=( a b c d e c )
length=${#array[#]}
current=0
for VALUE in "${array[#]}"; do
current=$((current + 1))
if [[ "$current" -eq "$length" ]]; then
echo "$VALUE is the last"
else
echo "$VALUE"
fi
done
This yields the output:
a
b
c
d
e
c is the last
This ensures that only the last item in the array triggers the alternative action and that, if any other item in the array duplicates the last value, the alternative action is not called for the earlier duplicates.
In the case of an array of paths to files in a specific directory, e.g.
array=( $DIR/* )
...it is probably less of a concern, since individual filenames within the same directory are almost-certainly unique (unless you have a really odd filesystem!)
You can abuse the positional parameters, since they act similarly to an array,
but are a little easier to manipulate. You should either save the old positional
parameters, or execute in a subshell.
# Method 1: use a subshell. Slightly cleaner, but you can't always
# do this (for example, you may need to affect variables in the current
# shell
files=( $DIR/* )
(
set -- "${files[#]}"
until (( $# == 1 )); do
doSomethingRegular "$1"
shift
done
doSomethingSpecial "$1"
)
# Method 2: save the positional parameters. A bit uglier, but
# executes everything in the same shell.
files=( $DIR/* )
oldPP=( "$#" )
set -- "${files[#]}"
until (( $# == 1 )); do
doSomethingRegular "$1"
shift
done
doSomethingSpecial "$1"
set -- "${oldPP[#]}"
What makes a file the last one? Is there something special about it? Is it the file with the greatest name when sorted by name?
Maybe you can take the file names backwards. Then, it's the first file you want to treat special and not the last. figuring out the first is a much easier task than doing the last:
for file in $(ls -r1 $dir)
do
if [ ! $processedLast ]
then
doSomethingSpecial($file)
processedLast=1
else
doSomethingRegular($file)
fi
done
No arrays needed. Actually, I like chepner's answer about using positional parameters.
It's old question - but building on answer from #GregReynolds please use this one-liner if commands differ only by parameters on last pass. Ugly, ugly code for one-liner lovers
( ff="" ; for f in * "" ; do [ -n "$ff" ] && echo $(${f:+false} && echo $ff alternate params here || echo normal params $ff ) ; ff=$f ; done )
normal params 1
normal params 2
normal params 3
4 alternate params here

Resources