How to extract the rows under a column using shell script - shell

I have following output of linux command brctl show. I would like to extract all the associated interfaces (as given in the interface column) and store it in some array. Can somebody suggest a way to achieve this?
root#XXXX:~# brctl show
bridge name bridge id STP enabled interfaces
br-lan 7fff.00c0ca7e0288 no eth0
wlan1_1
wlan1_1.sta1
I am missing the exact formatting for some reason. But the output of brctl show looks pretty much like above with the exception of a few spacings.
So I would like to store eth0,wlan1_1, w;an1_1.sta1 in some array if possible.
Thanks

You can use cut and tail. You may have to adjust the number:
brctl show | tail -n +2 | cut -c 36-
If the tab characters are converted to spaces in your output, you will need:
brctl show | tail -n +2 | cut -c 46-
Bash script solution
Note: I tested with a datafile of your data, but it should work with the brctl show output as well:
#!/bin/bash
declare -i cnt=0
declare -a ifaces
while read -r line || [ -n "$line" ]; do
[ $cnt -eq 0 ] && { ((cnt++)); continue; }
ifaces+=( "${line//* /}" )
done <<<$(brctl show)
printf "%s\n" ${ifaces[#]}
tested with done <"$1" as substitute:
$ bash brctl.sh dat/brctl.txt
eth0
wlan1_1
wlan1_1.sta1

Shell script solution, more compatible and with less failure possibilities:
#!/bin/sh
BridgeMembers ()
{
local TheBridge="$1"
local CurrentLine=""
local CurrentMac=""
local CurrentNIC=""
IFS="$(printf "\n\b")" ; for CurrentLine in $(brctl showmacs "$TheBridge" | tail -n +2) ; do unset IFS
IsLocal="$(OneWord () { echo $3; }; OneWord $CurrentLine)"
if [ "$IsLocal" = "yes" ] ; then
CurrentMac="$(OneWord () { echo $2; }; OneWord $CurrentLine)"
CurrentNic="$(env LANG=en ifconfig | grep -e $CurrentMac)"
CurrentNic="$(OneWord () { echo $1; }; OneWord $CurrentNic)"
echo "$CurrentNic"
fi
done
}
BridgeMembers "$1"
Note that with "brctl show" you didn't get same column widths all times.

Related

can you use shell commands alter blocks of lines based on values in those lines?

I have a file that has multiple blocks of lines like so
line1
line1
-----
machine:chrome
purpose:
language:v2
request:v3
additional: v4
os:v4
-----
machine:firefox
purpose:
language:v2
request:v6
os:v4
-----
machine:helper
purpose:
language:v2
request:v8
os:v4
-----
another line
The lines don't necessarily have the same elements but they all start with machine and end with os.
I can only use shell commands so what I want to do is parse the line starting with machine in each block starting with machine and ending in os and use the parsed result is a command whose result is to be inserted in request.
so parse each line that has machine in it and use that value to run a different shell command with its result and then populate request with that result. As a challenge I was wondering if this could be done using only sed and awk.
My expected output for the above would be:
line1
line1
-----
machine:chrome
purpose:
language:v2
request:[output of result of ps -ef chrome | awk '{print $2}']
additional: v4
os:v4
-----
machine:firefox
purpose:
language:v2
request:[output of result of ps -ef firefox | awk '{print $2}']
os:v4
-----
machine:helper
purpose:
language:v2
request:[output of result of ps -ef helper | awk '{print $2}']
os:v4
-----
another line
Update:
Trying to do this in sed alone I got the following:
gsed -r '/machine/,/os/ {/machine/ {s/.*?:\s*([^\s]+)/ps -ef | grep \1\n/e}; /request/ {s/.*?:\s*([^\s]+)//}}' filename
Which does not work but It runs the ps -ef | grep [machinename] and stores it in the buffer. Now I'd like to know if I can use the buffer value in the request substitution and if so how?
A
Edit: Because of changed requirements I am updating the script. The folowing produces the required output:
#!/bin/bash
function processlines() {
local line machine request
# Skips first three lines, but it is not really necessary.
#for k in `seq 1 3`; do read line; echo "$line"; done
while true; do
read -r line || return 0
if echo "$line" | grep '^machine:' >> /dev/null; then
machine="$(echo "$line" | cut -d ':' -f 2)"
echo "$line"
elif echo "$line" | grep '^request:' >> /dev/null; then
request="$(echo YOUR COMMAND HERE "$machine")"
echo "request:$request"
else
echo "$line"
fi
done
}
processlines < test.txt
Note: This works as long as the fields appear in the order shown by you. If "request" appears before "machine" or if one of both is missing in the file, the script would break. Please let me know if this can be the case.
Old answer:
You don't need sed or awk for that. It's doable almost by pure bash + tail/cut:
cat test.txt | tail -n +4 | while read machineline; do
[[ "$machineline" == "another line" ]] && break
read purposeline
read languageline
read requestline
read osline
read separatorline
machine="$(echo $machineline | cut -d ':' -f 2)"
purpose="$(echo $purposeline | cut -d ':' -f 2)"
language="$(echo $languageline | cut -d ':' -f 2)"
request="$(echo $requestline | cut -d ':' -f 2)"
os="$(echo $osline | cut -d ':' -f 2)"
separator="$(echo $separatorline | cut -d ':' -f 2)"
# Here do anything with the variables...
echo "machine is '$machine'" \
"purpose is '$purpose'" \
"language is '$language'" \
"request is '$request'" \
"os is '$os'" \
"separator is '$separator'"
done
And if you need the "machine" value only, then it is way easier:
cat test.txt | grep '^machine:' | cut -d ':' -f 2 | while read machinevalue; do
# call your other command here...
echo "machine value is '$machinevalue'"
done
A word of caution: If your values contain the character ":" this script would break and then you would have to use sed 's/^machine://g' instead of cut -d ':' -f 2.
A possible optimization would be to use bash for extracting the parts of the string but I am too lazy for that and unless I need the performance, I prefer using shell commands because I remember them more easily.
Regarding I was wondering if this could be done using only sed and awk - no, it can't because the task requires a shell to call ps so any sed or awk script would need to spawn a subshell to call ps, they can't call it on their own. So if you tried to do that then in terms of calls you'd end up with something like shell { awk { system { subshell { ps } } } } (which clearly isn't only using awk anyway) instead of simply shell { ps }.
Using md5sum (a very common application for this technique) for the example instead of ps -ef which would produce different output on everyone's different machines, you can tweak it to use ps -ef later, you COULD do the following (but don't, see the 2nd script below for a better approach):
$ cat tst.sh
#!/usr/bin/env bash
infile="$1"
while IFS= read -r line; do
if [[ "$line" =~ ^([^:]+):(.*) ]]; then
tag="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
case "$tag" in
machine )
mac="$val"
;;
request )
val="$(printf '%s' "$mac" | md5sum | cut -d' ' -f1)"
;;
esac
line="${tag}:${val}"
fi
printf '%s\n' "$line"
done < "$infile"
$ ./tst.sh file
line1
line1
-----
machine:chrome
purpose:
language:v2
request:554838a8451ac36cb977e719e9d6623c
additional: v4
os:v4
-----
machine:firefox
purpose:
language:v2
request:d6a5c9544eca9b5ce2266d1c34a93222
os:v4
-----
machine:helper
purpose:
language:v2
request:fde5d67bfb6dc4b598291cc2ce35ee4a
os:v4
-----
another line
While the above would work it'd be very inefficient (see why-is-using-a-shell-loop-to-process-text-considered-bad-practice) since it's looping through every line of input using shell so the following is how I'd really approach a task like this as it's far more efficient since it only has shell loop through each of the machine: lines from the input (which is unavoidable and is a far smaller number of iterations than if it had to read every input line) and the rest is done with a single call to sed to generate the input for the shell loop and a single call to awk to produce the output:
$ cat tst.sh
#!/usr/bin/env bash
infile="$1"
sed -n 's/^machine://p' "$infile" |
while IFS= read -r mac; do
printf '%s\n%s\n' "$mac" "$(printf '%s' "$mac" | md5sum | cut -d' ' -f1)"
done |
awk '
NR==FNR {
if ( NR % 2 ) {
mac = $0
}
else {
map[mac] = $0
}
next
}
{
tag = val = $0
sub(/:.*/,"",tag)
sub(/[^:]*:/,"",val)
}
tag == "machine" { mac = val }
tag == "request" { $0 = tag ":" map[mac] }
{ print }
' - "$infile"
$ ./tst.sh file
line1
line1
-----
machine:chrome
purpose:
language:v2
request:554838a8451ac36cb977e719e9d6623c
additional: v4
os:v4
-----
machine:firefox
purpose:
language:v2
request:d6a5c9544eca9b5ce2266d1c34a93222
os:v4
-----
machine:helper
purpose:
language:v2
request:fde5d67bfb6dc4b598291cc2ce35ee4a
os:v4
-----
another line
Here's what each of the above steps does:
Get just the machine: lines and remove the machine: part so we can have shell loop through just the parts it needs to call some command (e.g. ps -ef or md5sum) on:
$ sed -n 's/^machine://p' "$infile"
chrome
firefox
helper
Loop through each of those lines producing a mapping from that word to the output of the shell command you need to run on it (we generate the mapping in pairs of lines so the subsequent awk can parse it robustly even if the machine name from the input contained :s):
$ sed -n 's/^machine://p' "$infile" |
while IFS= read -r mac; do
printf '%s\n%s\n' "$mac" "$(printf '%s' "$mac" | md5sum | cut -d' ' -f1)"
done
chrome
554838a8451ac36cb977e719e9d6623c
firefox
d6a5c9544eca9b5ce2266d1c34a93222
helper
fde5d67bfb6dc4b598291cc2ce35ee4a
Pass that mapping to awk which separates it into the part before the : (which I'm calling a tag) and the part after it (which I'm calling a value) and stores the mapping in an array:
NR==FNR {
if ( NR % 2 ) {
mac = $0
}
else {
map[mac] = $0
}
next
}
It now reads the input file again, then using that array to modify the request: lines before printing each line (populating tag and val this way instead of setting FS to : and using $1 and $2 so we can again handle any input that contains :s in other locations):
{
tag = val = $0
sub(/:.*/,"",tag)
sub(/[^:]*:/,"",val)
}
tag == "machine" { mac = val }
tag == "request" { $0 = tag ":" map[mac] }
{ print }
The above assumes the shell command output is a single line each time it's called.
In pure bash:
#!/bin/bash
while IFS= read -r line; do
if [[ $line = machine:* ]]; then mach=${line#*:}
elif [[ $line = os:* ]]; then mach=""; fi
if [[ $line = request:* && $mach ]]; then
printf 'request:'
your_command "$mach"
else
printf '%s\n' "$line"
fi
done < file
If the output of your command doesn't end with a newline character, then place an echo after your command.

Ping Script with filter

I have a text file with host names and IP addresses like so (one IP and one host name per row). The IP addresses and host names can be separated by a spaces and/or a pipe, and the host name may be before or after the IP address
10.10.10.10 HW-DL11_C023
11.11.11.11 HW-DL11_C024
10.10.10.13 | HW-DL12_C023
11.11.11.12 | HW-DL12_C024
HW-DL13_C023 11.10.10.10
HW-DL13_C024 21.11.11.11
HW-DL14_C023 | 11.10.10.10
HW-DL14_C024 | 21.11.11.11
The script below should be able to ping hosts with a common denominator e.g. DL13 (there are two devices and it will ping only those two). What am I doing wrong, as I simply can`t make it work?
The script is in the same directory as the data; I don`t get errors, and everything is formatted. The server is Linux.
pingme () {
hostfile="/home/rex/u128789/hostfile.txt"
IFS= mapfile -t hosts < <(cat $hostfile)
for host in "${hosts[#]}"; do
match=$(echo "$host" | grep -o "\-$1_" | sed 's/-//' | sed 's/_//')
if [[ "$match" = "$1" ]]; then
hostname=$(echo "$host" | awk '{print $2}')
ping -c1 -W1 $(echo "$host" | awk '{print $1}') > /dev/null
if [[ $? = 0 ]]; then
echo "$hostname is alive"
elif [[ $? = 1 ]]; then
echo "$hostname is dead"
fi
fi
done
}
Try adding these two lines to your code:
pingme () {
hostfile="/home/rex/u128789/hostfile.txt"
IFS= mapfile -t hosts < <(cat $hostfile)
for host in "${hosts[#]}"; do
echo "Hostname: $host" # <-------- ADD THIS LINE -------
match=$(echo "$host" | grep -o "\-$1_" | sed 's/-//' | sed 's/_//')
echo "...matched with $match" # <-------- ADD THIS LINE -------
if [[ "$match" = "$1" ]]; then
hostname=$(echo "$host" | awk '{print $2}')
ping -c1 -W1 $(echo "$host" | awk '{print $1}') > /dev/null
if [[ $? = 0 ]]; then
echo "$hostname is alive"
elif [[ $? = 1 ]]; then
echo "$hostname is dead"
fi
fi
done
}
Then when you run it, you should see a list of your hosts, at least.
If you don't then you're not reading your file successfully.
If you do, there's a problem in your per-host logic.
Congratulations! You've divided your problem into two smaller problems. Once you know which half has the problem, keep dividing the problem in half until the smallest possible problem is staring you in the face. You'll probably know the solution at that point. If not, add your findings to the question and we'll help out from there.
The original code doesn't handle the pipe separator or the possibly reversed hostname and IP address in the input file. It also makes a lot of unnecessary use of external programs (grep, sed, ...).
Try this:
# Enable extended glob patterns - e.g. +(pattern-list)
shopt -s extglob
function pingme
{
local -r host_denom=$1
local -r hostfile=$HOME/u128789/hostfile.txt
local ipaddr host tmp
# (Add '|' to the usual characters in IFS)
while IFS=$'| \t\n' read -r ipaddr host ; do
# Swap host and IP address if necessary
if [[ $host == +([0-9]).+([0-9]).+([0-9]).+([0-9]) ]] ; then
tmp=$host
host=$ipaddr
ipaddr=$tmp
fi
# Ping the host if its name contains the "denominator"
if [[ $host == *-"$host_denom"_* ]] ; then
if ping -c1 -W1 -- "$ipaddr" >/dev/null ; then
printf '%s is alive\n' "$host"
else
printf '%s is dead\n' "$host"
fi
fi
done < "$hostfile"
return 0
}
pingme DL13
The final line (call the pingme function) is just an example, but it's essential to make the code do something.
REX, you need to be more specific about your what IP's you are trying to get from this example. You also don't ping enough times IMO and your script is case sensitive checking the string (not major). Anyway,
First, check that your input and output is working correctly, in this example I'm just reading and printing, if this doesn't work fix permissions etc :
file="/tmp/hostfile.txt"
while IFS= read -r line ;do
echo $line
done < "${file}"
Next, instead of a function first try to make it work as a script, in this example I manually set "match" to DL13, then I read each line (like before) and (1) match on $match, if found I remove the '|', and then read the line into an array of 2. if the first array item is an a IP (contains periods) set it as the IP the other as hostname, else set the opposite. Then run the ping test.
# BASH4+ Example:
file="/tmp/hostfile.txt"
match="dl13"
while IFS= read -r line ;do
# -- check for matching string (e.g. dl13 --
[[ "${line,,}" =~ "${match,,}" ]] || continue
# -- We found a match, split out host/ip into vars --
line=$(echo ${line//|})
IFS=' ' read -r -a items <<< "$line"
if [[ "${items[0]}" =~ '.' ]] ;then
host="${items[1]}" ; ip="${items[0]}"
else
host="${items[0]}" ; ip="${items[1]}"
fi
# -- Ping test --
ping -q -c3 "${ip}" > /dev/null
if [ $? -eq 0 ] ;then
echo "$host is alive!"
else
echo "$host is toast!"
fi
done < "${file}"

replacing some commands in an existing BASH script

I have the following script which sends the results of an iwlist scan via OSC:
#!/bin/bash
NUM_BANKS=20
while [[ "$input" != "\e" ]] ; do
networks=$(iwlist wlan0 scanning | awk 'BEGIN{ FS="[:=]"; OFS = " " }
/ESSID/{
#gsub(/ /,"\\ ",$2)
#gsub(/\"/,"",$2)
essid[c++]=$2
}
/Address/{
gsub(/.*Address: /,"")
address[a++]=$0
}
/Encryption key/{ encryption[d++]=$2 }
/Quality/{
gsub(/ dBm /,"")
signal[b++]=$3
}
END {
for( c in essid ) { print "/wlan_scan ",essid[c],signal[c],encryption[c] }
}'
)
read -t 0.1 input
echo "$networks" | while read network; do
set $network
hash=` echo "$2" | md5sum | awk '{ print $1 }'| tr '[:lower:]' '[:upper:]'`
bank=`echo "ibase=16;obase=A; $hash%$NUM_BANKS " | bc`
echo "$1$bank $2 $3 $4"
echo "$1$bank $2 $3 $4" | sendOSC -h localhost 9997
done
#echo "$networks" | sendOSC -h localhost 9997
done
An example of the output from this is '/wlan_scan13 BTHomehub757 -85 On', which is then sent via the sendOSC program.
I basically need to replace the iwlist scan data with the results of this tshark scan:
sudo tshark -I -i en1 -T fields -e wlan.sa_resolved -e wlan_mgt.ssid -e radiotap.dbm_antsignal type mgt subtype probe
which similarly outputs two strings and an int, outputting a result like:
'Hewlett-_91:fa:xx EE-BrightBox-mjmxxx -78'.
So eventually I want the script to give me an output in this instance of
'/wlan13 Hewlett-_91:fa:xx EE-BrightBox-mjmxxx -78'.
Both scans constantly generate results in this format at about the same rate, updating as new wifi routers are detected, and these are sent out as soon as they arrive over the sendOSC program.
This is probably a pretty simple edit for an experienced coder, but I've been trying to work this out for days and I figured I should ask for help!
If someone could clarify what needs to stay and what needs to go here I'd really appreciate it.
Many thanks.
Do you really want to replace commands? The sane approach would seem to be to add an option to the script to specify which piece of code to run, and include them both.
# TODO: replace with proper option parsing
case $1 in
--tshark) command=tshark_networks; shift;;
*) command=iwlist_networks;;
esac
tshark_networks () {
sudo tshark -I -i en1 -T fields \
-e wlan.sa_resolved \
-e wlan_mgt.ssid \
-e radiotap.dbm_antsignal type mgt subtype probe
}
iwlist_networks () {
iwlist wlan0 scanning | awk .... long Awk script here ....
}
while [[ "$input" != "\e" ]] ; do
networks=$($command)
read -t 0.1 input
echo "$networks" | while read network; do
: the rest as before, except fix your indentation
This also has the nice side effect that the hideous iwlist command is encapsulated in its own function, outside of the main loop.
... Well, in fact, I might refactor the main loop to
while true; do
$command |
while read a b c d; do
hash=$(echo "$b" | md5sum | awk '{ print toupper($1) }')
bank=$(echo "ibase=16;obase=A; $hash%$NUM_BANKS " | bc)
echo "$a$bank $b $c $d"
echo "$a$bank $b $c $d" | sendOSC -h localhost 9997
done
read -t 0.1 input
case $input in '\e') break;; esac
done

take ping test average change output

Here is my script I wanto change out put second one:
#!/bin/bash
declare -a arr=("8.8.8.8" "8.8.4.4" "192.168.1.28")
x=0
DATE=`date +%Y-%m-%d:%H:%M:%S`
echo $DATE > denemesh.txt
while [ $x -le 2 ]
do
echo " ${arr[x]}" >> denemesh.txt
ping -c 4 ${arr[x]} | tail -1| awk ' {print $4 }' | cut -d '/' -f 2 >> denemesh.txt
x=$(( $x + 1 ))
done
Currently, the output looks like this:
2014-12-22:20:22:37
8.8.8.8
18.431
8.8.4.4
17.758
192.168.1.28
0.058
Is it possible to change to output to look like this instead?
2014-12-22:20:22:37
8.8.8.8 18.431
8.8.4.4 17.758
192.168.1.28 0.058
You really just need to modify one line:
echo -n " ${arr[x]}" >> denemesh.txt
Using the -n flag suppresses the trailing newline, and so your next statement should append to the current line. You can then adjust the formatting as you please.
Sure it is. Try something like this:
declare -a arr=("8.8.8.8" "8.8.4.4" "192.168.1.28")
d=$(date +%Y-%m-%d:%H:%M:%S)
echo "$d" > denemesh.txt
for ip in "${arr[#]}"
do
printf ' %-12s' "$ip"
ping -c 4 "$ip" | awk 'END{split($4,a,"/"); printf "%12s\n", a[2]}'
done >> denemesh.txt
I've used printf with format specifiers to align the output. The %-12s left-aligns the first column with a fixed width of 12 characters and the %12s in awk right-aligns the second column. Rather than use a while loop, I got rid of your variable x and have looped through the values in the array directly. I have also changed the old-fashioned backtick syntax in your script to use $( ) instead. awk is capable of obtaining the output directly by itself, so I removed your usage of tail and cut too. Finally, you can simply redirect the output of the loop rather than putting >> on the end of each line.

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

Resources