Mask output card number with ****

Viewed 325

I have a task to mask the first 12 digits of each credit card number with an asterisk (*) given an input file and print the masked number to an output file.

Example card numbers:

1111-2222-3333-4444
4444-3333-2222-1111
1234-5678-9101-1171
1234 5678 9101 1121
7347_9834_7598_2834
8973#9858#3475#8734
2356`7843`0527`5340
8734=7583=4895=7007
8763+2430+6257_9406

Everything should be done in a shell script

My solution is:

#!/bin/bash

file='cards.txt'
while read data; do
echo $data | sed -r 's/[[:digit:]]{4}/****/;s/[[:digit:]]{4}/****/;s/[[:digit:]]{4}/****/;s/[^0-9,*]+/ /g'
done < $file > cards-masked.txt

Any better ideas on how to use sed in this task?

6 Answers

Looking at the example data, it seems you have always 4 digits separated by a character other than a digit.

If you are interested in using an awk solution as well, you can replace all chars other than a digit with a space first.

Then replace all chars in the first 3 columns with *

awk '{gsub(/[^0-9]+/, " ");for (i=1;i<4;i++) gsub(/[0-9]/,"*",$i)}1' cards.txt > cards-masked.txt

A bit more readable version with a short explanation

awk '{
  gsub(/[^0-9]+/, " ")                    # Replace all chars other than 0-9 with a space
  for (i=1;i<4;i++) gsub(/[0-9]/,"*",$i)  # Loop the first 3 columns, replace all digits with *
}1' cards.txt > cards-masked.txt          # The 1 evaluates to true, printing the whole line

Output

**** **** **** 4444
**** **** **** 1111
**** **** **** 1171
**** **** **** 1121
**** **** **** 2834
**** **** **** 8734
**** **** **** 5340
**** **** **** 7007
**** **** **** 9406

1st solution: Using simple sed perform substitution 3 times of 4 digits to 4 * each time and you are all set.

sed -E 's/[[:digit:]]{4}/****/;s/[[:digit:]]{4}/****/;s/[[:digit:]]{4}/****/' Input_file


2nd solution: With awk program it could be simply done by using 3 times sub(substitute) function.

awk '{sub(/[[:digit:]]{4}/,"****");sub(/[[:digit:]]{4}/,"****");sub(/[[:digit:]]{4}/,"****")} 1' Input_file

If your input always have a non-digit separator after every 4th digit as shown in question then using gnu-awk, you can do it easily using RS and RT.

Using -v RS='[^0-9]' we split each record on non-digit character and using module we set each record to **** when record number is not divisible by 4. Finally using ORS=RT we place captured non-digit character in output to get same delimiter as input.

awk -v RS='[^0-9]' 'NR%4 {$0="****"} {ORS=RT} 1' file > output

cat output

****-****-****-4444
****-****-****-1111
****-****-****-1171
**** **** **** 1121
****_****_****_2834
****#****#****#8734
****`****`****`5340
****=****=****=7007
****+****+****_9406

It is much simple:

cat cards.txt|rev|sed -e 's/[[:digit:]]/*/5g' |rev > cards-masked.txt

echo "1234-5678-9123-1234"|rev|sed -e 's/[[:digit:]]/*/5g' |rev
****-****-****-1234

This one is reverse task:

cat cards.txt|sed -e 's/[[:digit:]]/*/13g' > cards-masked.txt

As you can see there is no need in while/read loop too. Each one sed expression will be evaluated per-line.

This might work for you (GNU sed):

sed -E '/([0-9]{4}[ #`=+-_]){3}[0-9]{4}/{s//\n&\n/;h
       s/[0-9]/*/g;G;s/.*\n(.*)\n.*\n(.*)\n.*\n/\2\1/}' file

Surround a match by newlines.

Make a copy.

Replace all digits by *'s

Append the copy.

Using pattern matching, replace the original match by the amended match.

Can be extended for multiple matches on a line:

sed -E ':a;/([0-9]{4}[ #`=+-_]){3}[0-9]{4}/{s//\n&\n/;h
       s/[0-9]/*/g;G;s/.*\n(.*)\n.*\n(.*)\n.*\n/\2\1/;ta}' file

To replace only the first nine digits, use:

sed -E ':a;/(([0-9]{4}[ #`=+-_]){3})([0-9]{4})/{s//\n\1\n\3/;h
       s/[0-9]/*/g;G;s/.*\n(.*)\n.*\n(.*)\n.*\n/\2\1/;ta}' file

To replace the first 15 characters:

sed -e 's/^.\{15\}/**** **** **** /' < file

Output:

**** **** **** 4444
…
**** **** **** 9406

Same, but keeps characters at positions 5, 10, and 15:

sed -e 's/^....\(.\)....\(.\)....\(.\)/****\1****\2****\3/' < file

Output:

****-****-****-4444
…
****+****+****_9406
Related