2015-12-07 28 views
7

Ho un documento che mi serve per creare/aggiornare dinamicamente gli indici. Sto cercando di realizzare questo con awk. Ho un esempio di lavoro parziale, ma ora sono perplesso.awk dynamic document indexing

Il documento di esempio è il seguente.

numbers.txt: 
    #) Title 
    #) Title 
    #) Title 
    #.#) Subtitle 
    #.#.#) Section 
    #.#) Subtitle 
    #) Title 
    #) Title 
    #.#) Subtitle 
    #.#.#) Section 
    #.#) Subtitle 
    #.#.#) Section 
    #.#.#.#) Subsection 
    #) Title 
    #) Title 
    #.#) Subtitle 
    #.#.#) Section 
    #.#.#.#) Subsection 
    #.#.#.#) Subsection 

L'uscita desiderato sarebbe:

1) Title 
2) Title 
3) Title 
3.1) Subtitle 
3.1.1) Section 
3.2) Subtitle 
4) Title 
5) Title 
5.1) Subtitle 
5.1.1) Section 
5.2) Subtitle 
5.2.1) Section 
5.2.1.1) Subsection 
6) Title 
7) Title 
7.1) Subtitle 
7.1.1) Section 
7.1.1.1) Subsection 
7.1.1.2) Subsection 

Il codice awk che ho che funziona parzialmente è il seguente.

numbers.sh: 
    awk '{for(w=1;w<=NF;w++)if($w~/^#\)/){sub(/^#/,++i)}}1' number.txt 

Qualsiasi aiuto con questo sarebbe molto apprezzato.

+1

Penso che questa sarà una grande domanda per il colloquio. – karakfa

risposta

4

Ho implementato uno script AWK per te! E funzionerà ancora per più di quattro indici di livello! ;)

cercherò di spiegarlo un po 'con commenti in linea:

#!/usr/bin/awk -f 

# Clears the "array" starting from "from"          
function cleanArray(array,from){             
    for(w=from;w<=length(array);w++){           
     array[w]=0                
    }                   
}                    

# This is executed only one time at beginning.         
BEGIN {                   
    # The key of this array will be used to point to the "text index". 
    # I.E., an array with (1 2 2) means an index "1.2.2)"   
    array[1]=0  
}                    

# This block will be executed for every line.         
{                    
    # Amount of "#" found.              
    amount=0                  

    # In this line will be stored the result of the line.      
    line=""                  

    # Let's save the entire line in a variable to modify it.      
    rest_of_line=$0                

    # While the line still starts with "#"...         
    while(rest_of_line ~ /^#/){             

     # We remove the first 2 characters.          
     rest_of_line=substr(rest_of_line, 3, length(rest_of_line))    

     # We found one "#", let's count it!          
     amount++                 

     # The line still starts with "#"?          
     if(rest_of_line ~ /^#/){             
      # yes, it still starts.            

      # let's print the appropiate number and a ".".      
      line=line""array[amount]            
      line=line"."               
     }else{                 
      # no, so we must add 1 to the old value of the array.  
      array[amount]++              

      # And we must clean the array if it stores more values    
      # starting from amount plus 1. We don't want to keep     
      # storing garbage numbers that may harm our accounting    
      # for the next line.             
      cleanArray(array,amount + 1)           

      # let's print the appropiate number and a ")".      
      line=line""array[amount]            
      line=line")"               
     }                  
    }                   

    # Great! We have the line with the appropiate indexes!      
    print line""rest_of_line              
} 

Quindi, se si salva come script.awk, quindi è possibile eseguirlo l'aggiunta di permesso di esecuzione al File:

chmod u+x script.awk 

Infine, è possibile eseguirlo:

./script.awk <path_to_number.txt> 

Per fare un esempio, se si salva lo script script.awk nella stessa directory in cui si trova il file number.txt, quindi, cambiare la directory a quella directory ed eseguire:

./script.awk number.txt 

Così , se avete questo number.txt

#) Title 
#) Title 
#) Title 
#.#) Subtitle 
#.#.#) Section 
#.#) Subtitle 
#) Title 
#) Title 
#.#) Subtitle 
#.#.#) Section 
#.#) Subtitle 
#.#.#) Section 
#.#.#.#) Subsection 
#) Title 
#) Title 
#.#) Subtitle 
#.#.#) Section 
#.#.#.#) Subsection 
#.#.#.#.#) Subsection 
#.#.#.#.#) Subsection 
#.#.#.#.#) Subsection 
#.#.#.#.#.#) Subsection 
#.#.#.#.#) Subsection 
#.#.#.#.#.#) Subsection 
#.#.#.#.#.#) Subsection 
#.#.#.#.#.#) Subsection 
#.#.#.#.#.#) Subsection 
#.#.#.#.#) Subsection 
#.#.#.#) Subsection 
#.#.#) Section 

questo sarà l'uscita (si noti che la soluzione non è limitata dalla quantità di "#"):

1) Title 
2) Title 
3) Title 
3.1) Subtitle 
3.1.1) Section 
3.2) Subtitle 
4) Title 
5) Title 
5.1) Subtitle 
5.1.1) Section 
5.2) Subtitle 
5.2.1) Section 
5.2.1.1) Subsection 
6) Title 
7) Title 
7.1) Subtitle 
7.1.1) Section 
7.1.1.1) Subsection 
7.1.1.1.1) Subsection 
7.1.1.1.2) Subsection 
7.1.1.1.3) Subsection 
7.1.1.1.3.1) Subsection 
7.1.1.1.4) Subsection 
7.1.1.1.4.1) Subsection 
7.1.1.1.4.2) Subsection 
7.1.1.1.4.3) Subsection 
7.1.1.1.4.4) Subsection 
7.1.1.1.5) Subsection 
7.1.1.2) Subsection 
7.1.2) Section 

Spero che ti aiuti!

+1

Facundo, è meraviglioso che tu doni il tuo tempo in questo modo. Per mantenere la buona volontà e l'utilità in tutte le direzioni, posso raccomandarvi di aggiungere qualche spiegazione alla vostra risposta, in modo che funzioni come uno strumento educativo oltre che una semplice soluzione all'unico problema descritto nella domanda? – ghoti

+0

Grazie, ghoti! Certo, lascia che lo aggiorni. –

4

awk per il salvataggio!

Non sono sicuro che questo è il modo ottimale di fare questo, ma funziona ...

awk 'BEGIN{d="."} 
/#\.#\.#\.#/ {sub("#.#.#.#", i d a[i] d b[i d a[i]] d (++c[i d a[i] d b[i d a[i]]]))} 
    /#\.#\.#/ {sub("#.#.#" , i d a[i] d (++b[i d a[i]]))} 
     /#\.#/ {sub("#.#" , i d (++a[i]))} 
     /#/ {sub("#"  , (++i))} 1' 

UPDATE: È possibile che questo è limitato a solo 4 livelli. Ecco uno migliore per un numero illimitato di livelli

awk '{d=split($1,a,"#")-1;    # find the depth 
     c[d]++;        # increase counter for current   
     for(i=pd+1;i<=d;i++) c[i]=1;  # reset when depth increases 
     for(i=1;i<=d;i++) {sub(/#/,c[i])}; # replace digits one by one 
     pd=d} 1'       # set previous depth and print 

forse ripristinare passi può essere combinato con il ciclo principale, ma credo che in questo modo più chiaro.

UPDATE 2:

credo che con questa logica, il seguente è il più breve possibile.

$ awk '{d=split($1,_,"#")-1;  # find the depth 
     c[d]++;     # increment counter for current depth 
     for(i=1;i<=d;i++)   # start replacement 
      {if(i>pd)c[i]=1;  # reset the counters 
      sub(/#/,c[i])   # replace placeholders with counters 
      } 
      pd=d} 1' file   # set the previous depth 

o come una battuta

$ awk '{d=split($1,_,"#")-1;c[d]++;for(i=1;i<=d;i++){if(i>pd)c[i]=1;sub(/#/,c[i])}pd=d}1' 
1

Ecco un altro modo per farlo.

La spiegazione è fornita sotto il codice.

awk 'BEGIN {n0=1; prev=0} 
    {n1=split($1, elems, "."); # Get the number of pound signs 
    dif = (n1-n0);    # Increase in topic depth from previous line 
    scale = (10^dif);  # 10 raised to dif 
    current=(int(prev*scale)+1); # scale the number by change in depth 
    withdots=gensub(/([0-9])/, "\\1." , "g", current); # dot between digits 
    {print withdots, $2 } 
    n0=n1; 
    prev=current}' number.txt 


1) Title 
2) Title 
3) Title 
3.1) Subtitle 
3.1.1) Section 
3.2) Subtitle 
4) Title 

Considerare i numeri di argomento come numeri decimali.
otteniamo il numero corrente dalla precedente dalla formula 10^dif + 1,

dove dif = (Increase in number of levels from previous line) Inizialmente, dif è zero, in modo da ottenere 2 da 1 e 3 da 2,
da 1 * (10^0) +1 = 1 * 1 + 1 = 2
e 2 * (10^0) +1 = 2 * 1 + 1 = 3

allora otteniamo 31 da 3 da 3 * (10^1) + 1
012.351.641.061.32 da 311 per 311 * (10^-1) + 1 e così via

+0

Dovrebbe far notare che questa soluzione funziona in GNU awk (gawk) ma non in awk in OSX, FreeBSD, ecc. 'Gensub' non è portabile. – ghoti

2

gawk

awk 'function w(){ 
    k=m>s?m:s 
    for(i=1;i<=k;i++){ 
     if(i>m){ 
      a[i]=0 
     } 
     else{ 
      a[i]=(i==m)?++a[i]:a[i] #ended "#" increase 
      sub("#",a[i]=a[i]?a[i]:1) 
     } 
    } 
    s=m 
} 
{m=split($1,t,"#")-1;w()}1' file 



1) Title 
2) Title 
3) Title 
3.1) Subtitle 
3.1.1) Section 
3.2) Subtitle 
4) Title 
5) Title 
5.1) Subtitle 
5.1.1) Section 
5.2) Subtitle 
5.2.1) Section 
5.2.1.1) Subsection 
6) Title 
7) Title 
7.1) Subtitle 
7.1.1) Section 
7.1.1.1) Subsection 
7.1.1.2) Subsection 
2

stesso approccio di @ karakfa (breve e dolce) e con lo stesso avvertimento circa il numero massimo assunto delle sottovoci, ma un po 'più corto e più efficiente:

awk 'BEGIN{d="."} 
    /#\.#\.#\.#/ {sub("#.#.#.#", i d a d b d (++c))} 
    /#\.#\.#/ {sub("#.#.#" , i d a d (++b)); c=0;} 
     /#\.#/ {sub("#.#" , i d (++a));  b=0;} 
      /#/ {sub("#"  , (++i));   a=0;} 1' 
+0

intelligente che corregge il caos dell'array. – karakfa

2

Ecco la mia opinione su questo. Testato in FreeBSD, quindi mi aspetto di lavorare un po 'ovunque ...

#!/usr/bin/awk -f 

BEGIN { 
    depth=1; 
} 

$1 ~ /^#(\.#)*\)$/ { 
    thisdepth=split($1, _, "."); 

    if (thisdepth < depth) { 
    # end of subsection, back out to current depth by deleting array values 
    for (; depth>thisdepth; depth--) { 
     delete value[depth]; 
    } 
    } 
    depth=thisdepth; 

    # Increment value of last member 
    value[depth]++; 

    # And substitute it into the current line. 
    for (i=1; i<=depth; i++) { 
    sub(/#/, value[i], $0); 
    } 
} 

1 

L'idea di base è che manteniamo un array (value[]) dei nostri valori capitolo nidificate. Dopo aver aggiornato la matrice come richiesto, passiamo attraverso i valori, sostituendo la prima occorrenza dell'ottotipo (#) ogni volta con il valore corrente per quella posizione dell'array.

Questo gestirà qualsiasi livello di nidificazione e, come ho già detto, dovrebbe funzionare sia in GNU (Linux) che in versioni non GNU (FreeBSD, OSX, ecc.) Di awk.

E, naturalmente, se one-liners sono la vostra passione, questo può essere compresso:

awk -vd=1 '$1~/^#(\.#)*\)$/{t=split($1,_,".");if(t<d)for(;d>t;d--)delete v[d];d=t;v[d]++;for(i=1;i<=d;i++)sub(/#/,v[i],$0)}1' 

che potrebbe anche essere espressa, per facilitarne la lettura, in questo modo:

awk -vd=1 '$1~/^#(\.#)*\)$/{    # match only the lines we care about 
    t=split($1,_,".");     # this line has 't' levels 
    if (t<d) for(;d>t;d--) delete v[d]; # if levels decrease, trim the array 
    d=t; v[d]++;       # reset our depth, increment last number 
    for (i=1;i<=d;i++) sub(/#/,v[i],$0) # replace hash characters one by one 
    } 1'         # and print. 

UPDATE

E dopo aver riflettuto su questo per un po ', mi rendo conto che questo può essere ridotto ulteriormente. Il ciclo for contiene le sue condizioni, non è necessario collocarlo all'interno di uno if.E

awk '{ 
    t=split($1,_,".");     # get current depth 
    v[t]++;        # increment counter for depth 
    for(;d>t;d--) delete v[d];   # delete record for previous deeper counters 
    d=t;        # record current depth for next round 
    for (i=1;i<=d;i++) sub(/#/,v[i],$0) # replace hashes as required. 
    } 1' 

Quale di minifies corso in uno di linea come questa:

awk '{t=split($1,_,".");v[t]++;for(;d>t;d--)delete v[d];d=t;for(i=1;i<=d;i++)sub(/#/,v[i],$0)}1' file 

Ovviamente, è possibile aggiungere la condizione partita iniziale, se lo richiedono, in modo che solo le linee di processo che sembrano titoli .

Nonostante alcuni caratteri siano più lunghi, credo che questa versione sia sempre leggermente più veloce della soluzione simile di karakfa, probabilmente perché evita l'ulteriore if per ogni iterazione del ciclo for.

UPDATE # 2

includo questo perché questo perché ho trovato divertente e interessante. Puoi farlo solo in bash, senza bisogno di awk. E non è molto più in termini di codice.

#!/usr/bin/env bash 

while read word line; do 
    if [[ $word =~ [#](\.#)*\) ]]; then 
    IFS=. read -ra a <<<"$word" 
    t=${#a[@]} 
    ((v[t]++)) 
    for ((; d > t ; d--)); do unset v[$d]; done 
    d=t 
    for ((i=1 ; i <= t ; i++)); do 
     word=${word/[#]/${v[i]}} 
    done 
    fi 
    echo "$word $line" 
done < input.txt 

Questo segue la stessa logica dello script awk sopra, ma funziona interamente in bash utilizzando il parametro di espansione per sostituire # caratteri. Un difetto di cui soffre è che non mantiene gli spazi bianchi attorno alla prima parola su ogni riga, quindi perdi qualsiasi rientro. Con un po 'di lavoro, anche questo potrebbe essere mitigato.

Divertiti.