tags:

views:

299

answers:

6

I want to scan the passwd file and change the order of words in the comment field from firstname lastname to lastname firstname, and force the surname to capitals.

So, change every line from:

jbloggs:x:9999:99:Joe Bloggs:/home/jbloggs:/bin/ksh

to:

jbloggs:x:9999:99:BLOGGS Joe:/home/jbloggs:/bin/ksh

I'm new to Perl and I'm having problems with different field separators in awk. Appreciate any help.

+1  A: 

Stand-alone example:

use strict;
use warnings;
my $s = 'jbloggs:x:9999:99:Joe Bloggs:/home/jbloggs:/bin/ksh';
my @tokens = split /:/, $s;
my ($first, $last) = split /\s+/, $tokens[4];
$tokens[4] = uc($last) . " $first";
print join(':', @tokens), "\n";

__END__
jbloggs:x:9999:99:BLOGGS Joe:/home/jbloggs:/bin/ksh

As a script (output to STDOUT; must redirect output to a file):

use strict;
use warnings;
while (<>) {
    chomp;
    my @tokens = split /:/;
    my ($first, $last) = split /\s+/, $tokens[4];
    $tokens[4] = uc($last) . " $first";
    print join(':', @tokens), "\n";
}
toolic
This fails for comma delimited GECOS fields and gives a "Use of uninitialized value $first in concatenation (.) or string" error for some records.
Dennis Williamson
The problem is that there are too many special cases.
Dennis Williamson
+1  A: 
$ awk -F":" ' { split($5,a," ");$5=toupper(a[2])" "a[1] } 1 ' OFS=":" /etc/passwd
jbloggs:x:9999:99:BLOGGS Joe:/home/jbloggs:/bin/ksh
ghostdog74
Fails for GECOS fields that are comma delimited. Adds a space to the beginning of the GECOS field if it consists of only one word.
Dennis Williamson
let's not get too far ahead here.
ghostdog74
+1  A: 

$ awk -v FS=":" '{split($5, a, " "); name = toupper(a[2]) " " a[1]; gsub($5, name); print $0}' passwd

Won't work if you have middle names though.

Edit: easier to read version

awk -v FS=":" '
{
    split($5, name_parts, " ")
    name = toupper(name_parts[2]) " " name_parts[1]
    gsub($5, name)
    print $0
}' passwd
Steven Huwig
+1 just thought `awk`
honk
`$5 = name` works just fine, no need for `gsub`. Also, I think you want a `-v OFS=:` too. `print` with no argument prints `$0`, but I guess that's a matter of taste.
ephemient
`$5 = name` doesn't work without the `-v OFS=:`. If you don't include both you get the fields space-separated. `gsub()` replaces the contents of `$0` with the result, so in my script I don't need to worry about `OFS`.
Steven Huwig
Oh, I see what you're doing there now. Hmm, what if `$5 = "."`? Dangerous.
ephemient
Fails for empty GECOS fields, ones that contain only one word and those that are comma delimited.
Dennis Williamson
thanks everyone - this is great. What if the GECOS contains $firstname $lastname $otherstuff - eg - bloggs:x:9999:99:Joe Bloggs temp (Sales):/home/jbloggs:/bin/kshSo I just want to operate on the full name, as above, and discard the rest of the GECOS?
paul44
+1  A: 

This will process the file as you read it and put the new format entries into the array @newEntries.

open PASSWD, "/etc/passwd";
while(<PASSWD>) {
    @fields = split /:/;
    ($first, $last) = split (/\s/, $fields[4]);
    $last = uc $last;
    $fields[4] = "$last $first";
    push @newEntries, join(':', @fields);
}
HerbN
Shorter: `$fields[4] = "\U$last\E $first";`, skip the manual `uc`. Well, it's equivalent anyhow...
ephemient
Adds a space to empty or one-word GECOS fields. Fails for comma-delimited ones.
Dennis Williamson
+1 Good point...I focused too much on the specific example and didn't make it general.
HerbN
+2  A: 

Use Passwd::Unix or Passwd::Linux.

Sinan Ünür
+1  A: 
$ perl -pe 's/^((.*:){4})(\S+)\s+(\S+?):/$1\U$4\E, $3:/' \
    /etc/passwd >/tmp/newpasswd

To rewrite only those users who have logged in within the past sixty days according to lastlog, you might use

#! /usr/bin/perl -n

use warnings;
use strict;

use Date::Parse;

my($user) = /^([^:]+):/;
die "$0: $ARGV:$.: no user\n" unless defined $user;

if ($user eq "+") {
  next;
}

my $lastlog = (split " ", `lastlog -u "$user" | sed 1d`, 4)[-1];
die "$0: $ARGV:$.: lastlog -u $user failed\n"
  unless defined $lastlog;

my $time_t = str2time $lastlog;

# rewrites users who've never logged in
#next if defined $time_t && $time_t < ($^T - 60 * 24 * 60 * 60);

# users who have logged in within the last sixty days
next unless defined $time_t && $time_t >= ($^T - 60 * 24 * 60 * 60);

s/^((.*:){4})(\S+)\s+(\S+?):/$1\U$4\E, $3:/;
print;

as in

$ ./fixusers /etc/passwd >/tmp/fixedusers
Greg Bacon
Fails for comma delimited GECOS fields.
Dennis Williamson
@Dennis It probably fails for lots of formats that paul44 didn't mention in the question.
Greg Bacon
I have already cleaned up the GECOS field as far as I could - this works just fine for me now - thanks.Incidentally - is there an easy way to take the first field (username) and tell if he has logged in recently (say, comparing to a lastlog -u) and only output if the user has logged in in, say, tha last 60 days, so stipping out users who have been idle for 2 months?
paul44
@paul44 Answer updated!
Greg Bacon
Format of a lastlog is below. Does perl have a funky way of checking the last login date and excluding any who have never logged in or not for 60 days?[root@server1 paul]# lastlog -u bsmithUsername Port From Latestbsmith pts/2 172.21.83.181 Mon Oct 19 16:10:56 +0100 2009[root@itdev5 paul]# lastlog -u jbloggsUsername Port From Latestjbloggs **Never logged in**All the responses above have been really useful - just this last favour, please - cheers.
paul44
@paul44 See the "rewrites users who've never logged in" comment? Uncomment the following line, and comment out the "next unless..." a couple of lines down.
Greg Bacon