Tips en Trucs 2018

Op zoek naar niet afgesloten haken

Heb je ooit een tekst met bijna een half miljoen regels (iets meer dan 300 MB) gecontroleerd op niet gesloten haken. M.a.w. wordt elke geopende haakje ook terug gesloten?

Elke regel bevat bovendien 77 velden, gescheiden door tab-tekens. We zoeken dus naar niet afgesloten haken binnen elk veld. De volgende opdracht klaarde de klus op een Intel i3 dual core processor met 4 GB RAM geheugen in 1 minuut en 13 seconden. Niet slecht als je weet dat je daarvoor 36.036.077 velden moet controleren.

awk -F"\t" '{for (i=1;i<=NF;i++) if (split($i,a,"(") != split($i,b,")")) {print NR": "$0; next}}' tekstbestand

Hoe werkt het?

Om uit te leggen hoe de opdracht werkt, maken we een testbestand met drie door tab-tekens gescheiden velden.

dany@pindabook:~> cat > demo
a(aaa)  bbbb    (cccc(
dddd    e(e(ee) fff)f
gg(g)g  hhh(h   ii(i)i
 

Om het demo bestand af te sluiten en weg te schrijven, druk je Ctrl + d.

AWK heeft een split functie met drie of vier argumenten. Het eerste argument bevat de te splitsen tekenreeks. Het tweede argument bevat de naam van het array waarin de gesplitste delen worden opgeslagen. Het derde argument bevat het teken (of tekenreeks) die een splitsing veroorzaken. Het vierde optioneel argument bevat de naam van het array waarin de tekens van elke splitsing die de splitsing veroorzaakten wordt opgeslagen. Uiteindelijk geeft split het aantal gesplitste delen terug.

Met volgende opdracht wordt elke regel gesplitst bij het ( teken en wordt per regel het aantal gesplitste delen weergegeven:

dany@pindabook:~> awk '{print split($0,a,"(")}' demo
4
3
4

Je merkt dat AWK onze eerste regel in vier delen heeft gesplitst, namelijk a, aaa)[tab]bbbb[tab], cccc en een leeg deel na de laatste (. AWK maakt voor het uitvoeren van de opdracht de array a eerst leeg. Herhalen we de opdracht met splitsen op een ) teken, dan bekomen we:

dany@pindabook:~> awk '{print split($0,a,")")}' demo
2
3
3

Nu kunnen we AWK het aantal in een regel gevonden geopende en gesloten haken met elkaar laten vergelijken. Zijn er niet evenveel geopende als gesloten haken, dan wordt het regelnummer en zijn inhoud weergegeven.

dany@pindabook:~> awk '{if (split($0,a,"(") != split($0,a,")")) print NR": "$0}' demo 
1: a(aaa)       bbbb    (cccc(
3: gg(g)g       hhh(h   ii(i)i

Regel twee bevat echter twee velden met niet afgesloten haken. Om binnen de velden te kijken wordt een for lus gebruikt. Als in een veld een niet afgesloten haakje wordt gevonden, wordt het regelnummer en zijn inhoud weergegeven. Met next wordt dan de volgende regel onderzocht.

dany@pindabook:~> awk -F"\t" '{for (i=1;i<=NF;i++) if (split($i,a,"(") != split($i,b,")")) {print NR": "$0; next}}' demo
1: a(aaa)       bbbb    (cccc(
2: dddd e(e(ee) fff)f
3: gg(g)g       hhh(h   ii(i)i

Deze opdracht is niet perfect, het vindt geen verkeerd geplaatste haakjes, zoals in )kkk(k. Daarvoor breiden we ons demo bestand uit met een extra regel (\t zorgt voor Tab ruimte):

dany@pindabook:~> echo -e "jjjj\t)kkk(k\tllll" >> demo
dany@pindabook:~> cat demo
a(aaa)  bbbb    (cccc(
dddd    e(e(ee) fff)f
gg(g)g  hhh(h   ii(i)i
jjjj    )kkk(k  llll

Met een AWK opdracht met een ingewikkelde reguliere expressie gaan we op zoek naar verkeerd geplaatste haken:

dany@pindabook:~> awk -F"\t" '{for (i=1;i<=NF;i++) if ($i ~ /^[^()]*\)[^()]*\(/) print NR": "$0}' demo
4: jjjj )kkk(k  llll

Met de sed editor kunnen we de betreffende regel weergeven om deze te onderzoeken:

dany@pindabook:~> sed -n '4p' demo
jjjj    )kkk(k  llll

En te repareren:

dany@pindabook:~> sed -i -e "4s/)kkk(/(kkk)/" demo
dany@pindabook:~> sed -n '4p' demo
jjjj    (kkk)k  llll

Voor programmeurs

Wie regelmatig programmeert of webpagina's maakt, weet hoe belangrijk haakjes zijn (Adobe noemt zijn webeditor niet voor niets Brackets). Laten we onze AWK opdracht los op een webpagina om de {} paren te controleren, dan bekom je het volgende resultaat:

dany@pindabook:~> awk -F"\t" '{for (i=1;i<=NF;i++) if (split($i,a,"{") != split($i,b,"}")) {print NR": "$0; next}}' Documenten/Web/index.html 
44:               (function() {
48:               })();
96: function verzenden(){
102: }

Hoewel de geteste webpagina geen fouten bevat, worden toch vier regels weergegeven. Dit komt doordat de haken geopend worden op twee regels (44 en 96) en terug gesloten op andere regels (48 en 102). Dit is voor een programmeur of ontwerper al een hele hulp.

Het kan echter nog beter met het volgende script:

#!/bin/bash

# Itentify the script
bname="$(basename "$0")"
# Make a work dir
wdir="/tmp/$USER/$bname"
[[ ! -d "$wdir" ]] && mkdir -p "$wdir"

# Arg1: The bracket pair 'string'
pair="$1"
# pair='[]' # test
# pair='<>' # test
# pair='{}' # test
# pair='()' # test

# Arg2: The input file to test
ifile="$2"
  # Build a test source file
#  ifile="$wdir/$bname.in"
#  cp /dev/null "$ifile"
#  while IFS= read -r line ;do
#    echo "$line" >> "$ifile"
#  done <<EOF
#AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#[   ]    [         [         [
#<   >    <         
#                   <         >         
#                             <    >    >         >
#----+----1----+----2----+----3----+----4----+----5----+----6
#{   }    {         }         }         }         } 
#(   )    (         (         (     )   )                    
#ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
#EOF

echo "File = $ifile"
# Count how many: Left, Right, and Both
left=${pair:0:1}
rght=${pair:1:1}
echo "Pair = $left$rght"
# Make a stripped-down 'skeleton' of the source file - brackets only
skel="/tmp/$USER/$bname.skel" 
cp /dev/null "$skel"
# Make a String Of Brackets file ... (It is tricky manipulating bash strings with []..
sed 's/[^'${rght}${left}']//g' "$ifile" > "$skel"
< "$skel" tr  -d '\n'  > "$skel.str"
Left=($(<"$skel.str" tr -d "$left" |wc -m -l)); LeftCt=$((${Left[1]}-${Left[0]}))
Rght=($(<"$skel.str" tr -d "$rght" |wc -m -l)); RghtCt=$((${Rght[1]}-${Rght[0]}))
yBkts=($(sed -e "s/\(.\)/ \1 /g" "$skel.str"))
BothCt=$((LeftCt+RghtCt))
eleCtB=${#yBkts[@]}
echo

if (( eleCtB != BothCt )) ; then
  echo "ERROR:  array Item Count ($eleCtB)"
  echo "     should equal BothCt ($BothCt)"
  exit 1
else
  grpIx=0            # Keep track of Groups of nested pairs
  eleIxFir[$grpIx]=0 # Ix of First Bracket in a specific Group
  eleCtL=0           # Count of Left brackets in current Group 
  eleCtR=0           # Count of Right brackets in current Group
  errIx=-1           # Ix of an element in error.
  for (( eleIx=0; eleIx < eleCtB; eleIx++ )) ; do
    if [[ "${yBkts[eleIx]}" == "$left" ]] ; then
      # Left brackets are 'okay' until proven otherwise
      ((eleCtL++)) # increment Left bracket count
    else
      ((eleCtR++)) # increment Right bracket count
      # Right brackets are 'okay' until their count exceeds that of Left brackets
      if (( eleCtR > eleCtL )) ; then
        echo
        echo "ERROR:  MIS-matching Right \"$rght\" in Group $((grpIx+1)) (at Bracket $((eleIx+1)) overall)"
        errType=$rght    
        errIx=$eleIx    
        break
      elif (( eleCtL == eleCtR )) ; then
        echo "*INFO:  Group $((grpIx+1)) contains $eleCtL matching pairs"
        # Reset the element counts, and note the first element Ix for the next group
        eleCtL=0
        eleCtR=0
        ((grpIx++))
        eleIxFir[$grpIx]=$((eleIx+1))
      fi
    fi
  done
  #
  if (( eleCtL > eleCtR )) ; then
    # Left brackets are always potentially valid (until EOF)...
    # so, this 'error' is the last element in array
    echo
    echo "ERROR: *END-OF-FILE* encountered after Bracket $eleCtB."
    echo "        A Left \"$left\" is un-paired in Group $((grpIx+1))."
    errType=$left
    unpairedCt=$((eleCtL-eleCtR))
    errIx=$((${eleIxFir[grpIx]}+unpairedCt-1))
    echo "        Group $((grpIx+1)) has $unpairedCt un-paired Left \"$left\"."
    echo "        Group $((grpIx+1)) begins at Bracket $((eleIxFir[grpIx]+1))."
  fi

  # On error, get Line and Column numbers
  if (( errIx >= 0 )) ; then
    errLNum=0    # Source Line number (current).
    eleCtSoFar=0 # Count of bracket-elements in lines processed so far.
    errItemNum=$((errIx+1)) # error Ix + 1 (ie. "1 based")
    # Read the skeketon file to find the error line-number
    while IFS= read -r skline ; do
      ((errLNum++))
      brackets="${skline//[^"${rght}${left}"]/}" # remove whitespace
      ((eleCtSoFar+=${#brackets}))
      if (( eleCtSoFar >= errItemNum )) ; then
        # We now have the error line-number
        # ..now get the relevant Source Line 
        excerpt=$(< "$ifile" tail -n +$errLNum |head -n 1)
        # Homogenize the brackets (to be all "Left"), for easy counting
        mogX="${excerpt//$rght/$left}"; mogXCt=${#mogX} # How many 'Both' brackets on the error line? 
        if [[ "$errType" == "$left" ]] ; then
          # R-Trunc from the error element [inclusive]
          ((eleTruncCt=eleCtSoFar-errItemNum+1))
          for (( ele=0; ele<eleTruncCt; ele++ )) ; do
            mogX="${mogX%"$left"*}"   # R-Trunc (Lazy)
          done
          errCNum=$((${#mogX}+1))
        else
          # errType=$rght
          mogX="${mogX%"$left"*}"   # R-Trunc (Lazy)
          errCNum=$((${#mogX}+1))
        fi
        echo "  see:  Line, Column ($errLNum, $errCNum)"
        echo "        ----+----1----+----2----+----3----+----4----+----5----+----6----+----7"  
        printf "%06d  $excerpt\n\n" $errLNum
        break
      fi
    done < "$skel"
  else
    echo "*INFO:  OK. All brackets are paired."
  fi
fi
exit

Dit wordt het resultaat:

Brackets script