Bash scripting; confused with for loop - bash

I need to make a for loop that loops for every item in a directory.
My issue is the for loop is not acting as I would expect it to.
cd $1
local leader=$2
if [[ $dOpt = 0 ]]
then
local items=$(ls)
local nitems=$(ls |grep -c ^)
else
local items=$(ls -l | egrep '^d' | awk '{print $9}')
local nitems=$(ls -l | egrep '^d' | grep -c ^)
fi
for item in $items;
do
printf "${CYAN}$nitems\n${NONE}"
let nitems--
if [[ $nitems -lt 0 ]]
then
exit 4
fi
printf "${YELLOW}$item\n${NONE}"
done
dOpt is just a switch for a script option.
The issue I'm having is the nitems count doesn't decrease at all, it's as if the for loop is only going in once. Is there something I'm missing?
Thanks

Goodness gracious, don't rely on ls to iterate over files.
local is only useful in functions.
Use filename expansion patterns to store the filenames in an array.
cd "$1"
leader=$2 # where do you use this?
if [[ $dOpt = 0 ]]
then
items=( * )
else
items=( */ ) # the trailing slash limits the results to directories
fi
nitems=${#items[#]}
for item in "${items[#]}" # ensure the quotes are present here
do
printf "${CYAN}$((nitems--))\n${NONE}"
printf "${YELLOW}$item\n${NONE}"
done
Using this technique will safely handle files with spaces, even newlines, in the name.

Try this:
if [ "$dOpt" == "0" ]; then
list=(`ls`)
else
list=(`ls -l | egrep '^d' | awk '{print $9}'`)
fi
for item in `echo $list`; do
... # do something with item
done

Thanks for all the suggestions. I found out the problem was changing $IFS to ":". While I meant for this to avoid problems with whitespaces in the filename, it just complicated things.

Related

How to match a folder name and use it in an if condition using grep in bash?

for d in */ ; do
cd $d
NUM = $(echo ${PWD##*/} | grep -q "*abc*");
if [[ "$NUM" -ne "0" ]]; then
pwd
fi
cd ..
done
Here I'm trying to match a folder name to some substring 'abc' in the name of the folder and check if the output of the grep is not 0. But it gives me an error which reads that NUM: command not found
An error was addressed in comments.
NUM = $(echo ${PWD##*/} | grep -q "*abc*"); should be NUM=$(echo ${PWD##*/} | grep -q "*abc*");.
To clarify, the core problem would be to be able to match current directory name to a pattern.
You can probably simply the code to just
if grep -q "*abc*" <<< "${PWD##*/}" 2>/dev/null; then
echo "$PWD"
# Your rest of the code goes here
fi
You can use the exit code of the grep directly in a if-conditional without using a temporary variable here ($NUM here). The condition will pass if grep was able to find a match. The here-string <<<, will pass the input to grep similar to echo with a pipeline. The part 2>/dev/null is to just suppress any errors (stderr - file descriptor 2) if grep throws!
As an additional requirement asked by OP, to negate the conditional check just do
if ! grep -q "*abc*" <<< "${PWD##*/}" 2>/dev/null; then

Find number of files with prefixes in bash

I've been trying to count all files with a specific prefix and then if the number of files with the prefix does not match the number 5 I want to print the prefix.
To achieve this, I wrote the following bash script:
#!/bin/bash
for filename in $(ls); do
name=$(echo $filename | cut -f 1 -d '.')
num=$(ls $name* | wc -l)
if [$num != 5]; then
echo $name
fi
done
But I get this error (repeatedly):
./check_uneven_number.sh: line 5: [1: command not found
Thank you!
The if statement takes a command, runs it, and checks its exit status. Left bracket ([) by itself is a command, but you wrote [$num. The shell expands $num to 1, creating the word [1, which is not a command.
if [ $num != 5 ]; then
Your code loops over file names, not prefixes; so if there are three file names with a particular prefix, you will get three warnings, instead of one.
Try this instead:
# Avoid pesky ls
printf '%s\n' * |
# Trim to just prefixes
cut -d . -f 1 |
# Reduce to unique
sort -u |
while IFS='' read -r prefix; do
# Pay attention to quoting
num=$(printf . "$prefix"* | wc -c)
# Pay attention to spaces
if [ "$num" -ne 5 ]; then
printf '%s\n' "$prefix"
fi
done
Personally, I'd prefer case over the clunky if here, but it takes some getting used to.

Extract a certain part of a string in bash with different patterns

I have this file:
CLUSTERS=SP1,SP2,SP3
FNAME_SP1="REWARDS_BTS_SP1_<GTS>.dat"
FNAME_SP2="DUMP_LOG_SP2_<GTS>.dat"
FNAME_SP3="TEST_CASE_TABLE_SP3_<GTS>.dat"
What I want to get from these are:
REWARDS_BTS_SP1_
DUMP_LOG_SP2_
TEST_CASE_TABLE_SP3_
I loop through the CLUSTERS field, get the values, and use it to find the appropriate FNAME_<CLUSTERNAME> value. Basically, the CLUSTERS value are ALWAYS before the _<GTS> part of the string. Any string pattern will do, provided that the CLUSTERS value come before the _<GTS> at the end of the string.
Any suggestions? Here's a part of the script.
function loadClusters() {
for i in `echo ${!CLUSTER*}`
do
CLUSTER=`echo ${i} | grep $1`
if [[ -n ${CLUSTER} ]]; then
CLUSTER=${!i}
break;
fi
done
echo -e ${CLUSTER}
}
function loadClustersCampaign() {
for i in `echo ${!BPOINTS*}`
do
BPOINTS=`echo ${i} | grep $1`
if [[ -n ${BPOINTS} ]]; then
BPOINTS=${!i}
break;
fi
done
for i in `echo ${!FNAME*}`
do
FNAME=`echo ${i} | grep $1`
if [[ -n ${FNAME} ]]; then
FNAME=${!i}
break;
fi
done
echo -e ${BPOINTS}"|"${FNAME}
}
#get clusters
clusters=$(loadClusters $1)
for i in `echo $clusters | sed 's/,/ /g'`
do
file=$(loadClustersCampaign ${i/-/_} | awk -F"|" '{print $2}') ;
echo $file;
#then get the part of the $file variable
done
Fun with Shell Parameter Expansions
You can use matching-prefix notation and indirect expansion to get at the variables you want, and use the "remove suffix" expansion on each result to collect just the portions of the filename that you want. For example:
FNAME_SP1='REWARDS_BTS_SP1_<GTS>.dat'
FNAME_SP2='DUMP_LOG_SP2_<GTS>.dat'
FNAME_SP3='TEST_CASE_TABLE_SP3_<GTS>.dat'
for cluster in "${!FNAME_SP#}"; do
echo ${!cluster%%<GTS>*}
done
This will print out the following:
REWARDS_BTS_SP1_
DUMP_LOG_SP2_
TEST_CASE_TABLE_SP3_
but you could issue any valid shell command inside the loop instead of using echo.
See Also
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
If you like an awk solution for this ,may be below will be useful.
> echo 'FNAME_SP1="REWARDS_BTS_SP1_<GTS>.dat"' | awk -F"<GTS>" '{split($1,a,"=\"");print substr(a[2],2)}'
REWARDS_BTS_SP1_
Furthur more detail below:
> cat temp
LUSTERS=SP1,SP2,SP3
FNAME_SP1="REWARDS_BTS_SP1_<GTS>.dat"
FNAME_SP2="DUMP_LOG_SP2_<GTS>.dat"
FNAME_SP3="TEST_CASE_TABLE_SP3_<GTS>.dat"
> awk -F"<GTS>" '/FNAME_SP/{split($1,a,"=");print substr(a[2],2)}' temp
REWARDS_BTS_SP1_
DUMP_LOG_SP2_
TEST_CASE_TABLE_SP3_
>

bash script and greping with command line

new to bash scripting so just wondering if i am doing this code right at all. im trying to search /etc/passwd and then grep and print users.
usage ()
{
echo "usage: ./file.sk user"
}
# test if we have two arguments on the command line
if [ $# != 1 ]
then
usage
exit
fi
if [[ $# < 0 ]];then
usage
exit
fi
# Search for user
fullname=`grep $1 /etc/passwd | cut -f 5 -d :`
firstname=`grep $1 /etc/passwd | cut -f 5 -d : | cut -f 1 -d " "`
#check if there. if name is founf: print msg and line entry
not sure as how to this or if im doing this right...
am i doing this right?
grep $1 /etc/passwd | while IFS=: read -r username passwd uid gid info home shell
do
echo $username: $info
done
This might work for you:
fullname=$(awk -F: '/'$1'/{print $5}' /etc/passwd)
firstname=${fullname/ *}
You're on the right track.
But I think the 2nd if [[ $# < 0 ]] .... fi block doesn't get you much. Your first test case gets the situation right, 'This script requires 1 argument or quits'.
Also, I don't see what you need firstname for, so a basic test is
case "${fullname:--1}" in
-[1] ) printf "No userID found for input=$1\n" ; exit 1 ;;
* )
# assume it is OK
# do what every you want after this case block
;;
esac
You can of course, duplicate this using "${firstname}" if you really need the check.
OR as an equivalent if ... fi is
if [[ "${fullname}" == "" ]] ; then
printf "No userID found for input=$1\n" ; exit 1
fi
note to be more efficient, you can parse ${fullname} to get firstname without all the calls to grep etc, i.e.
firstname=${fullname%% *}
Let me know if you need for me to explain :--1} and %% *} variable modifiers.
I hope this helps.
Instead of this:
fullname=`grep $1 /etc/passwd | cut -f 5 -d :`
firstname=`grep $1 /etc/passwd | cut -f 5 -d : | cut -f 1 -d " "`
Try this:
fullname=$(cut -f5 -d: /etc/passwd | grep "$1")
if [[ $? -ne 0 ]]; then
# not found, do something
fi
firstname=${fullname%% *} # remove the space and everything after
Note that I changed my answer to cut before grep so that it doesn't get false positives if some other field matches the full name you are searching for.
You can simply by reading your input to an array and then printing out your desired fields, something like this -
grep $1 /etc/passwd | while IFS=: read -a arry; do
echo ${arry[0]}:${arry[4]};
done
Test:
jaypal:~/Temp] echo "root:*:0:0:System Administrator:/var/root:/bin/sh" |
while IFS=: read -a arry; do
echo ${arry[0]}:${arry[4]};
done
root:System Administrator

best way to find top-level directory for path in bash

I need a command that will return the top level base directory for a specified path in bash.
I have an approach that works, but seems ugly:
echo "/go/src/github.myco.com/viper-ace/psn-router" | cut -d "/" -f 2 | xargs printf "/%s"
It seems there is a better way, however all the alternatives I've seen seem worse.
Thanks for any suggestions!
One option is using awk:
echo "/go/src/github.myco.com/viper-ace/psn-router" |
awk -F/ '{print FS $2}'
/go
As a native-bash approach forking no subshells and invoking no other programs (thus, written to minimize overhead), which works correctly in corner cases including directories with newlines:
topdir() {
local re='^(/+[^/]+)'
[[ $1 =~ $re ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}
Like most other solutions here, invocation will then look something like outvar=$(topdir "$path").
To minimize overhead even further, you could pass in the destination variable name rather than capturing stdout:
topdir() {
local re='^(/+[^/]+)'
[[ $1 =~ $re ]] && printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
...used as: topdir "$path" outvar, after which "$outvar" will expand to the result.
not sure better but with sed
$ echo "/go/src/github.myco.com/viper-ace/psn-router" | sed -E 's_(/[^/]+).*_\1_'
/go
Here's a sed possibility. Still ugly. Handles things like ////////home/path/to/dir. Still blows up on newlines.
$ echo "////home/path/to/dir" | sed 's!/*\([^/]*\).*!\1!g'
/home
Newlines breaking it:
$ cd 'testing '$'\n''this'
$ pwd
/home/path/testing
this
$ pwd | sed 's!/*\([^/]*\).*!/\1!g'
/home
/this
If you know your directories will be rather normally named, your and anubhava's solutions certainly seem to be more readable.
This is bash, sed and tr in a function :
#!/bin/bash
function topdir(){
dir=$( echo "$1" | tr '\n' '_' )
echo "$dir" | sed -e 's#^\(/[^/]*\)\(.*\)$#\1#g'
}
topdir '/go/src/github.com/somedude/someapp'
topdir '/home/somedude'
topdir '/with spaces/more here/app.js'
topdir '/with newline'$'\n''before/somedir/somefile.txt'
Regards!

Resources