2

I'm using GNU sed 4.7 on Debian 11.

I have a file with many lines, all of which have a first string in curly brackets at the beginning of the line, a final string in @ chars at the end of the line, and a string containing alphanumeric, punctuation and other charcaters between the aforementioned termini strings.

I want to generate output showing the first and last strings in their entirity, and ONLY the non-alphanum chars from the middle string, thus:

./file contains the following:

{string-no1}middle@string-no2@
{AAAAAAAAAA}1,a.B£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:3m?J@@@@@@@@@@@@

... and I would like to see output:

{string-no1}@string-no2@
{AAAAAAAAAA},.£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:?@@@@@@@@@@@@

I tried:

sed 's/\({[^}]*}\)[^a-zA-Z0-9]*\(@[^@]*@\)/\1\2/' ./file

...but that doesn't work, and neither does:

sed 's/\({[^}]*}\)[[:punct:]]*\(@[^@]*@\)/\1\2/' ./file`

...nor does:

sed '/}/,/@/ s/[a-zA-Z0-9]*//' ./file

I've tried finding help with grymoire, and on StackExchange too, which usually solves anything, but this one's really got me foxed. Can anybody help?

1
  • The rightmost substring in your third example line does not match the pattern which seems to assume a substring of non-@ characters flanked by a single @ on either side. (@[^@]*@ does not match the entire length of @@@@@@@@@@@@). It doesn't make difference for the outcome, but it is unexpected.
    – Kusalananda
    Jan 29 at 9:06

4 Answers 4

2

This is difficult to do in sed (because you need to do different things - nothing, modify with s///, and nothing again - to three different parts of each input line) but very easy to do in perl.

$ perl -lne '($first,$middle,$last) = (/({[^}]*})([^@]*)(@.*)/);
             $middle =~ s/[[:alnum:]]+//g;
             print $first, $middle, $last' file 
{string-no1}@string-no2@
{AAAAAAAAAA},.£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:?@@@@@@@@@@@@

First it uses a regex to extract the first, middle, and last portions of the input line into appropriately named variables. Then it removes all alphanumeric characters from $middle. Then it prints them.

2

Your attempts do not work because the infix string (the middle bit) contains a mix of alphanumeric and non-alphanumeric characters. This infix must be processed with s/[[:alnum:]]//g, while avoiding doing the same operation to the prefix and suffix strings.

You, therefore, need to isolate the infix string in a variable, or, in the case of sed, in the editing buffer, apply the operation that deletes alphanumeric characters to it, and then re-apply the prefix and suffix strings to the result.

With a sed editing script:

h
s/^{[^}]*}//
s/@[^@]*@$//
s/[[:alnum:]]//g
G
s/^\(.*\)\n\({[^}]*}\).*\(@[^@]*@\)$/\2\1\3/

Testing:

$ sed -f script file
{string-no1}@string-no2@
{AAAAAAAAAA},.£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:?@@@@@@@@@@@@

Note that the infix string on the last line is actually

&:?@@@@@@@@@@

and that the suffix is

@@

Annotated script:

# Remember the original line in the hold space.
h

# Remove the prefix and the suffix strings.
# The prefix is "{...}" at the start of the line.
# The suffix is "@...@" at the end of the line.
# The interior of these strings does not contain
# the respective string terminator.
s/^{[^}]*}//
s/@[^@]*@$//

# We are left with the isolated infix portion of the
# original line. Remove the alphanumerical characters
# from this. This creates the final infix string.
s/[[:alnum:]]//g

# Append the original line from the hold space to the end of
# the infix string with a newline (\n) as the delimiter.
G

# Match the modified infix, prefix, and suffix only, and
# substitute the entire buffer with these parts in the
# correct order.
s/^\(.*\)\n\({[^}]*}\).*\(@[^@]*@\)$/\2\1\3/
2

With perl, you can also do:

perl -lne 'print /^\{.*?\}|@.*|\W/g' < your-file

\W matches characters other than alnum and underscore (only the ASCII ones by default). You can replace with [^a-zA-Z0-9] or [^[:alnum:]] if you'd like underscore to be included.

With sed, you could remove alnum characters between The first } and the first @ after that in a loop with:

sed -e :1 -e 's/^\([^}]*}[^@]*\)[[:alnum:]]/\1/; t1' < your-file

For sed, [[:alnum:]] are those classified as such in the locale and with the text decoded as per the locale's charset, while with perl by default, text is interpreted as if encoded in iso8859-1 and [[:alnum:]] only matches on the ASCII alnums (as long as you don't add the /u flag).

You can get a behaviour similar to perls in sed, by fixing the locale to C (LC_ALL=C sed...), and a behaviour similar sed's in perl, by adding the -Mopen=locale option, that will decode characters as per the locale charset and use Unicode properties (not the locale classification) to classify characters.

0

Here is an awk equivalent, in case you want to avoid the regex:

cat file.txt
{string-no1}@string-no2@
{AAAAAAAAAA},.£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:?@@@@@@@@@@@@

One can leave the comments when copy-pasting, as awk can handle them internally

awk '
   BEGIN{                             # sets '}' as field separator
        FS="}"                        # splitting text into 2 fields 
   }                                  
   { 
    i=index($2,"@")                   # finds index position of '@'
    str1=$1                           # str1 = 1st field
    str2=substr($2,0,i-1)             # str2 = 2nd field until first '@' 
    str3=substr($2,i,length($2))      # str3 = 2nd field from '@' till end
    gsub(/[[:alnum:]]/,"",str2)       # replaces alphan in str2 with blanks
    print str1"}"str2 str3            # combines str1,str2,str3 and prints

}' file.txt

Output:

{string-no1}@string-no2@
{AAAAAAAAAA},.£@ZZZZZZZZZZ@
{GGGGGGGGGG}&:?@@@@@@@@@@@@

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.