1

I am trying to create a script that can transform an output from a Cisco switch showing last input and output on an interface to a useful format. This is to be able to know what interfaces hasn't been used, let's say, the last 6 months. Cisco just says that an interfaces last input was 10w4d.

What I have come to so far is an input-file like this:

FastEthernet0/4,3 days ago,3 days ago
FastEthernet0/8,46 years ago ,46 years ago 
FastEthernet0/10,46 years ago ,46 years ago 
FastEthernet0/11,46 years ago ,46 years ago 
FastEthernet0/12,08:47:53,04:00:32
FastEthernet0/13,1 year ago 46 weeks ago ,1 year ago 46 weeks ago 
FastEthernet0/14,46 years ago ,46 years ago 
FastEthernet0/15,13 weeks ago 4 days ago ,13 weeks ago 4 days ago 
FastEthernet0/16,46 years ago ,46 years ago 
FastEthernet0/18,46 years ago ,46 years ago 
FastEthernet0/19,46 years ago ,46 years ago 
FastEthernet0/20,46 years ago ,46 years ago 
FastEthernet0/22,46 years ago ,1 year ago 50 weeks ago 
FastEthernet0/24,46 years ago ,46 years ago

I have set all interfaces that has the status of never on last input and output to 46 years ago, so I can get a valid date. And yes, I do have a record of uptime to be able to take that into account.

My issue now is that I am trying to convert that into a valid date. Doing that through awk and xargs works fine, if I do like this:

awk -F, '{print $2}' tst | xargs -i date -d "{}" +%Y-%m-%d

I get a response like this:

2016-08-18
1970-08-21
1970-08-21
1970-08-21
2016-08-21
2014-10-03
1970-08-21
2016-05-18
1970-08-21
1970-08-21
1970-08-21
1970-08-21
1970-08-21
1970-08-21

But if I do like this, which in my mind should give the same result:

awk -F, -v OFS="," '{
    cmd2=("date \"+'%Y-%m-%d'\" -d \""$2 "\"")
    cmd2|getline d2
    print cmd2,d2
}' tst

I get a response like this:

date "+%Y-%m-%d" -d "3 days ago",2016-08-18
date "+%Y-%m-%d" -d "46 years ago ",1970-08-21
date "+%Y-%m-%d" -d "46 years ago ",1970-08-21
date "+%Y-%m-%d" -d "46 years ago ",1970-08-21
date "+%Y-%m-%d" -d "08:47:53",2016-08-21
date "+%Y-%m-%d" -d "1 year ago 46 weeks ago ",2014-10-03
date "+%Y-%m-%d" -d "46 years ago ",2014-10-03
date "+%Y-%m-%d" -d "13 weeks ago 4 days ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18
date "+%Y-%m-%d" -d "46 years ago ",2016-05-18

I added the cmd2 to the print command just now to see what it was trying to do. Just running that command gives me the correct date.

I guess I have missed something simple, but after trying over and over again for 3-4 hours now I would like to have another set of eyes on it.

Anybody out there that can point me in the right direction?

Updated! With just three lines of the input-file I wasn't getting any issues myself, as Stephen also pointed out. Sorry for posting an untested scenario.

  • I get the right results on my machine... date "+%Y-%m-%d" -d "1 year ago 46 weeks ago ",2014-10-03 date "+%Y-%m-%d" -d "46 years ago ",1970-08-21 date "+%Y-%m-%d" -d "13 weeks ago 4 days ago ",2016-05-18 – Stephen Harris Aug 21 '16 at 20:43
  • awk -F, '{system("date +%F -d \""$2"\"")}' tst – Costas Aug 21 '16 at 21:09
  • With just those three lines of the input file I didn't get errors myself either. Updated with more data to work with – Rune Aug 21 '16 at 21:16
  • cut -d, -f2 tst | date -I -f - – Costas Aug 21 '16 at 21:23
  • Costas, your system call solution works fine it seems, I do get all the dates correctly. Can I store the output from the systemcall to a variable? What I would like to do is to replace the relative dates in column 2 and 3 with real dates. – Rune Aug 21 '16 at 21:27
2

When you do:

cmd = "some command"
cmd | getline d2
cmd | getline d2

in awk, the first getline gets the first line (record) of the output of cmd (cmd is then started upon that first getline), and the second one gets the second line. Upon reaching the end of the output, getline returns 0.

In your case of cmd = "date -d \"46 years ago\"", since that command only outputs one line, the first getline returns that line in d2 (and a positive number), but the second reaches eof, so returns 0 and leaves d2 untouched.

Here, you'd need:

 cmd | getline d2; close(cmd)

So cmd be run each time.

Or store the output of cmd in a hash to avoid running the same command several times with the same arguments, like

if (!($2 in cache)) {cmd=...; cmd | getline cache[$2]; close(cmd)}
print cache[$2]

It's still a good idea to close cmd there to avoid leaving a great number of file descriptors open.


Note the ambiguity in close(cmd) when cmd is used both for getline < cmd and cmd | getline. Does it close the cmd command or the cmd file or both?

$ cat test.awk
BEGIN{
  OFS=","
  getline a1 < "uname"
  "uname" | getline b1
  close("uname")

  getline a2 < "uname"
  "uname" | getline b2
  print a1,b1,a2,b2
}
$ echo test > uname
$ mawk -f test.awk
test,Linux,test,Linux
$ bwk-awk -f test.awk
test,Linux,test,Linux
$ gawk -f test.awk
test,Linux,,Linux
$ busybox awk -f test.awk
test,,test,

(bwk-awk being the one maintained by Brian Kernighan (the k in awk), the behaviour is similar with awk implementations based on that like Solaris nawk or FreeBSD awk).

See how busybox awk doesn't let you use "uname" as both a file and command with getline and how close("uname") doesn't close the uname file in gawk.

So, it's a good idea to make sure you don't use a file and command with the same name at the same time. You can add a "\n" at the beginning or end of a command to make it unlikely that it be confused with a file.

Like:

 awk 'BEGIN {getline foo < $ENVIRON["FILE"]; "uname\n" | getline system}'

would avoid the problem if $FILE happens to be uname (not if $FILE is uname<newline> but that's less likely).

2

So this is interesting; it seems in this structure awk is remembering that it's made this call before and not doing it again, and so not passing any data to readline.

We can see this by doing strace -f -o xxx awk ....

Looking through the trace we see:

3401  execve("/bin/date", ["date", "+%Y-%m-%d", "-d", "3 days ago"], [/* 50 vars */]) = 0
3403  execve("/bin/date", ["date", "+%Y-%m-%d", "-d", "46 years ago "], [/* 50 vars */]) = 0
3405  execve("/bin/date", ["date", "+%Y-%m-%d", "-d", "08:47:53"], [/* 50 vars */]) = 0
3407  execve("/bin/date", ["date", "+%Y-%m-%d", "-d", "1 year ago 46 weeks ago "], [/* 50 vars */]) = 0
3409  execve("/bin/date", ["date", "+%Y-%m-%d", "-d", "13 weeks ago 4 days ago "], [/* 50 vars */]) = 0

So each unique date is passed once, but never again. This happens with GNU awk on CentOS 7 and mawk on Debian, so this seems to be expected behaviour.

A simple hack is to make each line unique; eg add a comment with the line number.

cmd2="date \"+'%Y-%m-%d'\" -d \""$2 "\" ; # " NR 

The resulting code looks something like:

awk -F, -v OFS="," '{cmd2="date \"+'%Y-%m-%d'\" -d \""$2 "\" ; # " NR ;
                     cmd2|getline d2;
                     print cmd2,d2}' x
date "+%Y-%m-%d" -d "3 days ago" ; # 1,2016-08-18
date "+%Y-%m-%d" -d "46 years ago " ; # 2,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 3,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 4,1970-08-21
date "+%Y-%m-%d" -d "08:47:53" ; # 5,2016-08-21
date "+%Y-%m-%d" -d "1 year ago 46 weeks ago " ; # 6,2014-10-03
date "+%Y-%m-%d" -d "46 years ago " ; # 7,1970-08-21
date "+%Y-%m-%d" -d "13 weeks ago 4 days ago " ; # 8,2016-05-18
date "+%Y-%m-%d" -d "46 years ago " ; # 9,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 10,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 11,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 12,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 13,1970-08-21
date "+%Y-%m-%d" -d "46 years ago " ; # 14,1970-08-21

A more complicated solution would be to cache results in an array and only call date if you haven't seen this date before.

  • Thanks for the troubleshooting Stephen, no wonder I didn't get it working. This helped me getting the information I needed, and can continue my project. For the record I am using Ubuntu 16.04.1 and running Gnu AWK version 4.1.3. – Rune Aug 22 '16 at 6:17
  • It's just that you need to do a close(cmd), otherwise, cmd|getline keeps reading remainder lines in the output of cmd (here eof after the first line as there's only one line). That's standard and expected behaviour of awk – Stéphane Chazelas Aug 22 '16 at 8:38
  • @StéphaneChazelas I learned something today :-) Thanks! – Stephen Harris Aug 22 '16 at 11:29
1

To replace the relative dates in column 2 and 3 with real dates

awk -F, '{for(i=2;i<4;i++){"date -I -d \""$i"\"" | getline a; $i=a}}1' OFS=, tst

I was suprised that direct getline $i didn't work: it change either one field of two or nothing. So I have to include additional variable a
With user function defined:

awk -F, '
    function cmd2(date){
        "date -I -d \""date"\"" | getline a
        return a
    }
    {
        $2=cmd2($2)
        $3=cmd2($3)
        print
    }' OFS=, tst

But if the function is defined with local variable cmd2(date, a) it show the same behavior as noted above.

For homogenous lines task can be easy done with GNU sed

tr ',' '\n' tst |
sed '1~3!{s/^/date -I -d "/;s/$/"/e;}' |
paste -sd ',,\n'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.