1

I need to extract a part of string that may appear 1 to n times in each line.

For instance, this would reflect what I need:

This [dbo].[something] is a text containing [dbo].[something_else], then okay?
And then, [dbo].[something] may appear just once.
But why, nothing prevents [dbo].[something] from appearing twice as [dbo].[something] here.
And then can be three times, as [dbo].[something] is [dbo].[anything] but [dbo].[elsewhere] here.
[dbo].[otherthing] depicts another scenario with just one and pattern heading line
Or, also [dbo].[ultra] with an arbitrary amount of [dbo].[references] but ending with [dbo].[pattern]

As you may have noticed, the pattern would be \[dbo\]\.\[[^]]+\]. For instance, from the text above, I would want a result of:

something something_else
something
something something
something anything elsewhere
otherthing
ultra references pattern

Then I can just inline everything (or append to a bash array) and filter duplicates, this shouldn't be an issue. I am just having trouble to figure out how to do this filter in a single sweep.

What I have here, results in extracting just the last match (it is obvious why when you are used to sed's "greedy" approach to pattern matching):

cat dborefs.txt | sed -E "s/(.*\[dbo\]\.\[([^]]+)\].*)*/\2/g"
something_else
something
something
elsewhere
otherthing
pattern

I could extract, then replace the patterns so that they no longer match, then extract again until I get no more matches, but that sounds just too cumbersome, all bash overhead considered; it would be best to be able to extract everything in a single call to sed. I feel this should be possible, just can't easily figure out how. Thinking this may be useful for others, I felt like sharing the matter here could prove fruitful for the community.

5
  • Welcome, Avenger. Nice to see you are making efforts to solve the problem, but to me it is very confusing to understand what is your goal after extracting the bits after [dbo].
    – Quasímodo
    May 21, 2020 at 10:47
  • You mean, my "real world goal"? I kind of tried to remove the actual goal from the question not to confuse (and maybe I reached the wrong end). But the thing is that I have sql dumps from MS-SQL server, each object in a file; then I want to extract references from files to idenfity which .sql files I should run first so I don't get classic "something is not defined" when I try to run a script -- or find cyclic references.
    – Avenger
    May 21, 2020 at 10:55
  • In other words: need to extract references from .sql files to resources introduced by other .sql files (the file names reflect the resource each file introduce) so I can determine a script run order.
    – Avenger
    May 21, 2020 at 10:57
  • No, I mean what is the expected result/output? What is it that you want help with? I could not identify it.
    – Quasímodo
    May 21, 2020 at 11:07
  • The result/output is in the question itself, the second code block. Sorry for not responding earlier, I found a way that seems to be the best one anyway -- unless somebody proves me wrong :P
    – Avenger
    May 26, 2020 at 16:29

8 Answers 8

1

To get a newline-delimited list of marked up strings:

$ grep -o '\[dbo\]\.\[[^]]*\]' file | cut -d . -f 2 | tr -d '[]'
something
something_else
something
something
something
something
anything
elsewhere
otherthing
ultra
references
pattern

The first grep only produces lines with [dbo].[word]. The cut gives us the [word] bit and the tr removes the [ and] from this.

To get the marked up strings grouped by the line they occur on:

$ sed -e 's/\][^.[]*\[/] [/g' -e 's/^[^[]*//' -e 's/[^]]*$//' -e 's/\[dbo\]\.\[\([^]]*\)\]/\1/g' file
something something_else
something
something something
something anything elsewhere
otherthing
ultra references pattern

The four substitutions used here are

  1. Delete everything between ] and [ that isn't a dot or a [ (actually, replace with a space; these are the spaces in the final output).
  2. Delete everything up to the first [.
  3. Delete everything after the last ].
  4. Extract the marked up words in what remains.
2
  • I wanted to avoid all that command chaining you suggested in the first part of your answer. The second part seems compatible to what I needed, thanks.
    – Avenger
    May 26, 2020 at 16:26
  • @Avenger You never said anything about avoiding pipelines in the question. I also don't think the pipes would prove to be slowing things down much, and you never say anything about performance considerations in the question either.
    – Kusalananda
    May 26, 2020 at 16:38
0

For now, what I could have that is (hopefully) much better than repeatedly calling sed was to "chain" replacements with placeholders that hopefully won't appear in the files.

cat dborefs.txt | sed -E "
 s/\[dbo\]\.\[([^]]+)\]/_-\1-_/g;
 s/(^|-_)([^_]+|_[^-])*(\$|_-)/ /g;
 s/(^ +| +\$)//g"

In other words:

  • first I'm getting all [dbo].[<extract>] and replacing with _-<extract>-_;
  • then replace any text before the first _-, between -_ and _-, and after the last -_ by a single whitespace character;
  • and then clean up whitespace characters in the begin and end of each line.

This gives the desired result, and I may be able to join it all in an array then filter with sort for unique entries. But I still think there should be a better way without chained sed commands.

3
  • I have looked at all answers and all so far offers more chained commands or other tools (awk, perl, tr, grep, cut) and chained tools. It may get awfully slow to chain/pipe commands together while I could be doing in a single app (maybe not so much on native linux, but on cygwin that gets pretty slow due to overhead to run piped commands)
    – Avenger
    May 26, 2020 at 16:09
  • Note that the pipe that you use is not necessary. sed is quite happy to take a filename as an argument.
    – Kusalananda
    May 26, 2020 at 16:34
  • Sure it was a matter of choice; besides like this I could quickly switch between cat file to echo a variable.
    – Avenger
    May 26, 2020 at 16:37
0

You could do this more easily in Perl, using a hash (associative array) to uniquify the matches:

$ perl -nE 'while ($_ =~ /\[dbo\]\.\[(.*?)\]/g) {$h{$1}++} }{ for $k (keys %h) {say $k}' dborefs.txt 
otherthing
anything
elsewhere
something
pattern
something_else
ultra
references

A similar approach is possible in GNU Awk, by repeated application of the match function:

$ gawk '{
    while (match($0,/\[dbo\]\.\[([^]]+)\]/,a)) {h[a[1]]++; $0 = substr($0,RSTART+RLENGTH)}
  } 
  END{
    for (k in h) print k
  }' dborefs.txt 
references
elsewhere
something
something_else
pattern
otherthing
anything
ultra

With other Awk implementations whose match function doesn't provide the array of capture groups, you'd need to trim the match:

while (match($0,/\[dbo\]\.\[([^]]+)\]/)) {h[substr($0,RSTART+7,RLENGTH-8)]++; $0 = substr($0,RSTART+RLENGTH)}
1
  • thanks for the answer, it's just I needed for this question the usage within sed. I hope this will be useful for people doing that on awk.
    – Avenger
    May 26, 2020 at 16:04
0

Employing gnu sed in extended regex mode (to save escaping with backslashes to some extent):

sed -re '
  /\n/q;G
  :a;s/\[dbo]\.\[([^]]+)](.*\n\1(\n|$))/\2/;ta
  :b;s/\[dbo]\.\[([^]]+)](.*)/\2\n\1/;tb
  s/^[^\n]*\n//;h
  $\!d;g;D
' dborefs.txt


awk -F'[][]' '
NF>2{
  for (i=1; i<=NF-2; i++)
    if ( $(i) $(i+1) == "dbo." )
      a[$(i+2)]
 }
 END { for (i in a) print i } 
' dborefs.txt

Output;

something
something_else
anything
elsewhere
otherthing
ultra
references
pattern

perl -lne '
  $h{$1}++ while /\[dbo]\.\[([^]]+)]/g;
  }{print for keys %h;
' dborefs.txt
1
  • just a note, using -r for extender regex is not portable (would break on MAC, for instance). It's not wrong to use if you are going to rely on GNU sed but should keep that in mind. ;)
    – Avenger
    May 26, 2020 at 16:15
0

Yet another method, tthis time employing multiple utilities. The sed portion of the pipeline extracts the pattersns whilst the awk portion uniquifies it also at the same time preserving the order in which they were first seen.

sed -Ee '
  /\n/{P;D;}
  s/\[dbo]\.\[([^]]+)]/\n\1\n/;D
' dborefs.txt | awk '!a[$0]++'
1
  • that's nice and should work fine, I just wanted to avoid command chaining.
    – Avenger
    May 26, 2020 at 16:27
0

Another fun one with recursion. However, it's not a one line sed command...

$ sed -e ':loop;
          s/[^|\[]*\[dbo\]\.\[\([a-z_]*\)\][^[]*/\1|/;
          t loop;
          s/|$//;
          s/|/ /g' testfile
something something_else  
something  
something something  
something anything elsewhere  
otherthing  
ultra references pattern
  • s/...: replace the beginning of the line until [dbo].[myname] by myname|
  • t loop: iterate if a substitution was made
  • s/|...: get rid of |, replace with space or end of line
1
  • I have just checked, and your :loop concept looks fun, but in the end the same result is achieved if I just replace that two loop lines to a "g" (in like s///g) for the first command. So except for that loop, it would be the answer here closest (and IMO compatible with) my answer. This should be useful if I needed to repeat a chain of replacements, though.
    – Avenger
    May 26, 2020 at 16:14
0

You were on the right track, using your \[dbo\]\.\[[^]]+\]

  • adding a capture parenthesis: \[dbo\]\.\[([^]]+)\]

  • adding an initial capture [^[]* ==> [^[]*\[dbo\]\.\[([^]]+)\]

  • surrounding it in a substitution group s/ ... /\1]/g

  • Removing the last ] and what follows: s/\][^]]*$//

  • and converting all remaining ] to spaces: s/\]/ /g

Will get you there:

sed -Ee 's/[^[]*\[dbo\]\.\[([^]]+)\]/\1]/g' \
     -e 's/\][^]]*$//' \
     -e 's/\]/ /g' file

will yield:

something something_else
something
something something
something anything elsewhere
otherthing
ultra references pattern
1
  • your answer has been the closest to what I need but I guess there's no "single-replace" solution for that (I have tested your answer against mine and performance and all, they are equivalent).
    – Avenger
    May 26, 2020 at 16:28
0

I was not very clear what your expected output was. Sometimes you talk of filtering dups, other times one per line.

This no chaining, all sed solution uses the most secure placeholder rather than - _

sed -Ee '
  s/\[dbo]\.\[([^]]+)]/\n\1\n\n/g
  s/(^|\n\n)[^\n]*//g
  y/\n/ /
' file

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .