CSV from iw output

Intro

A long and lingering issue with wireless 802.11 work in a UNIX context is filtering AP scans into a meaningful form such that the important bits (BSSID, ESSID, channel) can be used by shell scripts, PHP, etc.

This is something grappled by many with ‘iwlist’, but as it is now legacy (along with ‘iwconfig’) across Linux distributions, a migration of scripts to ‘iw’ is required to ensure portability.

Here is a short script that will take the output of a scan and place it into a formatted CSV with fields on a single line, like so:

BSSID, freq (MHz), RSSI (dBms), ESSID, channel

24:65:12:85:3a:45,2412,-76.00,FRITZ!Box 7312,1
00:90:3c:c0:01:01,2457,-48.00,noonicorn,10
c0:15:02:c7:d6:45,2412,-86.00,FRITZ!Box Fon WLAN 71,1
74:3b:70:6b:22:4c,2422,-79.00,Don’t Menschen IT,3
e8:9c:a6:3d:c6:1a,2412,-71.00,Superglue-c81a-rescue,1
00:25:fe:a7:af:1c,2417,-84.00,Funkwrecker,2
71:32:71:e7:25:bf,2417,-79.00,WLAN-E72519,2

It takes care of ESSIDs with awkward characters, such as ‘!’ and whitespace.

#!/bin/bash
# Scan APs with 'iw' and output to single-line. 
# CSV is in the format:
#   BSSID, freq (MHz), RSSI (dBms), ESSID, channel) 
# Tested with iw version 3.15 on OpenWrt and 3.4 on Debian GNU/Linux

DATA=/path/to/data
NIC=$(iw dev | grep Interface | awk '{ print $2 }')
TMPFILE=/tmp/scan.log
SCAN=$DATA/networks
S=1;E=5;

# Scrape 'iw' scan output and write to temporary file
iw dev $NIC scan | grep -E '^BSS|SSID|DS Parameter set: channel|signal:|freq:'\
| sed -e 's/BSS\ //' -e 's/(on.*$//'  -e 's/DS Parameter\ set:\ channel\ // ' \
-e 's/SSID:\ //' -e 's/freq://' -e 's/signal: //' -e 's/dBm//'\
    > $TMPFILE && LEN=$(cat $TMPFILE | wc -l)

# Count lines in 5's
for i in $(seq 1 $((LEN/5)))
    do
        LINES=$(sed -n "$S,$E p;$E q" $TMPFILE)
        # Test for hidden ESSIDs. Allow for ESSIDs with spaces
        if [ $(echo $LINES | awk '{print substr($0, index($0,$4))}' | sed \
            's/\(.*\)[[:space:]].*/\1/' | wc -c )  -gt 1 ]
            then
                # Replace all whitespace with commas except whitespace in
                # ESSID field
                echo $LINES | awk -F ' ' -v OFS=',' '{print $1, $2, $3, \
                substr($0, index($0,$4))}' | sed 's/\(.*\)[[:space:]]/\1,/'
        fi
        # Increment
            ((S+=5))
            ((E+=5))
    done > $SCAN
rm $TMPFILE

Save it out as scan.sh then:

$ chmod +x scan.sh
$ sudo ./scan.sh && cat /path/to/data/networks

Output can be read straight into variables in a script, like so:

#!/bin/bash

...

while IFS=, read BSSID FREQ RSSI ESSID CHANNEL
    do
        # Do something with these variables here. 
    done < /path/to/data/networks

Here is a simpler function which uses exclusively Bash internals and spawns only one sub-shell:

#!/bin/bash 
function iwScan() {
   # disable globbing to avoid surprises
   set -o noglob
   # make temporary variables local to our function
   local AP S
   # read stdin of the function into AP variable
   while read -r AP; do
     ## print lines only containing needed fields
     [[ "${AP//'SSID: '*}" == '' ]] && printf '%b' "${AP/'SSID: '}\n"
     [[ "${AP//'signal: '*}" == '' ]] && ( S=( ${AP/'signal: '} ); printf '%b' "${S[0]},";)
   done
   set +o noglob
}

iwScan <<< "$(iw wlan0 scan)"

Output:

-66.00,FRITZ!Box 7312
-56.00,ALICE-WLAN01
-78.00,o2-WLAN93
-78.00,EasyBox-7A2302
-62.00,dlink
-74.00,EasyBox-59DF56
-76.00,BELAYS_Network
-82.00,o2-WLAN20
-82.00,BPPvM

The function can be easily modified to provide additional fields by adding a necessary filter into the while read -r AP while-loop, eg:

[[ "${AP//'last seen: '*}" == '' ]] && ( S=( ${AP/'last seen: '} ); printf '%b' "${S[0]},";)

Output:

-64.00,1000,FRITZ!Box 7312
-54.00,492,ALICE-WLAN01
-76.00,2588,o2-WLAN93
-78.00,652,LN8-Gast
-72.00,2916,WHITE-BOX
-66.00,288,ALICE-WLAN
-78.00,800,EasyBox-59DF56
-80.00,720,EasyBox-7A2302
-84.00,596,ALICE-WLAN08

NOTE: The order of fields is the same as in the original iw output

Happy Bash smashing! ,)