4

The following command is throwing an unterminated substitute pattern error in bash:

eval $(echo "sed '" "s,@\("{a..u}{a..z}"\),\n\n\1,;" "'")

But not for everyone. Linux apparently works fine. Mac throws the unterminated substitute pattern error.

How can I reorganize to make this work?

Here's the entire bash command (the goal is to cleanly output current MySQL settings into my.cnf) :

{
  # Print version, user, host and time
  echo -e "# MYSQL VARIABLES {{{1\n##\n# MYSQL `
      mysql -V | sed 's,^.*\(V.*\)\, for.*,\1,'
      ` - By: `logname`@`hostname -f` on `date +%c`\n##"
  for l in {a..z}; do
      # Get mysql global variables starting with $l
      echo '#'; mysql -NBe "SHOW GLOBAL VARIABLES LIKE '${l}%'" |
      # Transorm it
      sed 's,\t,^= ,' |
      column -ts^ |
      tr "\n" '@' |
      eval $(echo "sed '" "s,@\("{a..u}{a..z}"\),\n\n\1,;" "'") |
      eval $(echo "sed '" "s,@\(innodb_"{a..z}{a..z}"\),\n\n\1,;" "'") |
      tr '@' "\n" |
      sed 's,^,# ,g'
  done
  echo -e "#\n##\n# MYSQL VARIABLES }}}1";
} | tee ~/mysql-variables.log
Ryan
  • 14,682
  • 32
  • 106
  • 179

3 Answers3

5

The default sed in OS X is an BSD version of sed. Just tested:

  • the above fails in OS X's default sed,
  • and works with the GNU version (gsed - installed from macports).

So, probably the BSD version doesn't handles such long substitution command series.

You can try use the next:

eval $(echo "perl -ple '" "s,@("{a..u}{a..z}"),\n\n\1,;" "'")

And maybe I didn't understand right your goal, but what is a wrong with a much simpler?

sed 's/@\([a-u][a-z]\)/\n\n\1/' #or
sed 's/@\("[a-u][a-z]"\)/\n\n\1/'

EDIT

Once again i'm only focused to the 1st code-line and not the whole solution. So created a bash/perl version what works without problems on OS X (with default OS X tools).

The next code

MYSQLCMD=/usr/local/mysql-5.6.16-osx10.7-x86_64/bin/mysql   #your path to mysql command

printf "# MYSQL VARIABLES {{{1\n##\n# MYSQL %s " "$($MYSQLCMD -V | sed 's/.*\(Ver .*\),.*/\1/')"
printf " - By: %s@%s on %s\n" $(logname) $(hostname -f) "$(date +%c)"

perl -e "\$s=qx($MYSQLCMD -NBe 'SHOW GLOBAL VARIABLES');" \
     -e 'for("aa".."uz"){$s=~s/^($_)/#\n$1/m;$s=~s/^(innodb_$_)/#\n$1/m};' \
     -e '$s=~s/(.*)\t(.*)/sprintf "# %-55s= %s",$1,$2/gem;print $s'

printf "#\n##\n# MYSQL VARIABLES }}}1\n";

roughly do the same what the original code.

clt60
  • 62,119
  • 17
  • 107
  • 194
  • 1
    +1, but using the character class inside of `sed` is far less interesting than letting bash do the expansion! (And a whole lot smarter!) – William Pursell Jul 20 '14 at 18:52
  • 1
    While I agree your `sed` command is much simpler, I think we want each substitution happening through the `{a..u}{a..z}` expansion to occur only once and in given order. The idea behind this is to break file into stanzas containing lines starting with `aa…`, then `ab…`, etc. So we want to add new lines only before first occurence of pairs of letters. – Qeole Jul 20 '14 at 21:50
  • 1
    @Ryan added an full solution – clt60 Jul 21 '14 at 00:48
  • 1
    Awesome work @jm666! Is it possible to command-line-fu that into a one-liner? – Ryan Jul 21 '14 at 17:13
  • @Ryan don't know commandline.fu, but feel free, ok :) Honestly, probably it can be done much better... I'm not an perl-expert ;) – clt60 Jul 21 '14 at 18:57
1

Try breaking the command up into multiple commands:

 eval "$(printf "sed "; echo "-e 's,@\("{a..z}{a..z}"\),\n\n\1,'")"

But note that sed on OSX also probably doesn't like \n as a newline, so you'll have to do:

$ nl='
'
$ eval "$(printf "sed "; echo "-e 's,@\("{a..z}{a..z}"\),\\$nl\\$nl\1,'")"

I would strongly recommend finding a better solution. Probably via perl.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • Your fix solves the substitute error (even the first one), but when I run the entire script, it replaces t's for some reason. `wait` becomes `wai` and `thread` becomes `hread`. Can you see where/why this would happen? – Ryan Jul 20 '14 at 18:44
  • 1
    @Ryan, It replaces `t` because you are substituting it out with `=` ! – l'L'l Jul 20 '14 at 18:47
  • 1
    That replacement is happening because of the line `sed 's,\t,^= ,'`. You need to replace `\t` with a literal tab, similar to the way I replaced `\n` with a literal newline. – William Pursell Jul 20 '14 at 18:47
  • 1
    Different sed treat `\t` and `\n` differently, and this is a good reason to use a different tool. – William Pursell Jul 20 '14 at 18:48
  • 2
    I don't think the issue of differing support for control-character escape sequences such as `\n` and `\t` _by itself_ is a good enough reason to avoid `sed`: [ANSI-C quoting](http://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting) in `bash` (and also `ksh` and `zsh`) allows splicing in _literal_ control characters instead, which should work on all platforms. E.g., `sed 's,\t,^= ,'` would have to be written as `sed 's,'$'\t'',^= ,'`. – mklement0 Jul 21 '14 at 03:24
1

For a quick fix, try (see below for a preferable alternative that doesn't use eval):

eval "$(echo "sed '" "s,@\("{a..u}{a..z}"\),"$'\\\n\\\n'"\1,"$'\n' "'")"

As @jm666 hints at, FreeBSD sed (at least the version that comes with OS X 10.9.4) has a limit on the size of individual lines in a script (command string) - 4096 bytes - and the large single-line string that results from your use of bash's brace (range) expansion ({a..u}{a..z}) exceeds that limit.

The above works around that by putting each s call on its own line by appending $'\n' (which in bash expands to an actual newline - see below) rather than ; to the string to be brace-expanded.

Also note that \n\n was replaced with spliced-in $'\\\n\\\n', because FreeBSD sed doesn't support \n escapes in replacement strings (treats them as literal n chars). $'\\\n\\\n' inserts actual newlines - escaped with \ - using a bash feature called ANSI C-quoting.

(Similarly, FreeBSD sed also doesn't support escape sequence \t in regexes to represent chars, so your sed 's,\t,^= ,' command must be replaced with sed 's,'$'\t'',^= ,'.)

Note that the entire string passed to eval must then be double-quoted so as to ensure that the newlines are passed through to sed.

Note that you could in theory still hit a limit: the max. length of a command line, but that limit is much higher: a little less than 256 KB on OS X. Also, you may pass long sed scripts via a file, by using the -f option.


Generally, it's better to avoid use of eval, so here's an alternative:

 sed "$(printf %s "s,@\("{a..u}{a..z}"\),"$'\\\n\\\n'"\1,"$'\n')"
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thanks for your explanation, I understand at last why I couldn't reproduce bug on [original question](https://stackoverflow.com/questions/24851077) under Linux. – Qeole Jul 20 '14 at 21:45
  • 1
    @Qeole: My pleasure; thanks for explaining why jm666's sed-only solution may not be the right approach after all (I'll revise my answer). – mklement0 Jul 20 '14 at 22:27
  • 2
    I proposed semething [there](http://stackoverflow.com/a/24855726/3716552), in case you're interested. – Qeole Jul 20 '14 at 23:01