Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I convert a CMake semicolon-separated list to newline-separated?

Tags:

cmake

E.g:

set (txt "Hello" "There" "World")
# TODO
message (txt) # Prints "Hello\nThere\nWorld" (i.e. each list item on a new line

What do I put in place of TODO?

like image 378
Timmmm Avatar asked Jan 05 '23 18:01

Timmmm


2 Answers

CMake's lists are semicolon-delimited. So "Hello" "There" "World" is internally represented as Hello;There;World. So a simple solution is to replace semicolons with newlines:

string (REPLACE ";" "\n" txt "${txt}")

This works in this example, however lets try a more complicated example:

set (txt "" [[\;One]] "Two" [[Thre\;eee]] [[Four\\;rrr]])

The [[ ]] is a raw string so the \'s are passed through into CMake's internal representation of the list unchanged. The internal representation is: ;\;One;Two;Thre\;eee;Four\\;rrr. We'd expect it to print:

<blank line>
;One
Two
Thre;eee
Four\\;rrr

I'm not actually 100% sure about the Four\\;rrr one but I think it is right. Anyway with our naive implementation we actually get this:

<blank line>
\
One
Two
Thre\
eee
Four\\
rrr

It's because it doesn't know to not convert actual semicolons that are escaped. The solution is to use a regex:

string (REGEX REPLACE "[^\\\\];" "\\1\n" txt "${txt}")

I.e. only replace ; if it is preceded by a non-\ character (and put that character in the replacement). The almost works, but it doesn't handle the first empty element because the semicolon isn't preceded by anything. The final answer is to allow the start of string too:

string (REGEX REPLACE "(^|[^\\\\]);" "\\1\n" txt "${txt}")

Oh and the \\\\ is because one level of escaping is removed by CMake processing the string literal, and another by the regex engine. You could also do this:

string (REGEX REPLACE [[(^|[^\\]);]] "\\1\n" txt "${txt}")

But I don't think that is clearer.

Maybe there is a simpler method than this but I couldn't find it. Anyway, that, Ladies and Gentlemen, is why you should never use strings as your only type, or do in-band string delimiting. Still, could have been worse - at least they didn't use spaces as a separator like Bash!

like image 92
Timmmm Avatar answered Jan 17 '23 16:01

Timmmm


I just wanted to add some alternatives I'm seeing just using the fact that message() does place a newline at the end by itself:

  1. Just using for_each() to iterate over the list:

    set (txt "Hello" "There" "World")
    foreach(line IN LISTS txt)
        message("${line}")
    endforeach() 
    
  2. An function() based alternative I came up with looks more complicated:

    function(message_cr line)
        message("${line}")
        if (ARGN)
            message_cr(${ARGN})
        endif()
    endfunction()        
    
    set(txt "Hello" "There" "World")
    message_cr(${txt})
    

The more generalized version of those approaches would look like:

  1. for_each() with strings

    set(txt "Hello" "There" "World")
    foreach(line IN LISTS txt)
        string(APPEND multiline "${line}\n")
    endforeach() 
    message("${multiline}")
    
  2. function() with strings

    function(stringify_cr var line)
        if (ARGN)
            stringify_cr(${var} ${ARGN})
        endif()
        set(${var} "${line}\n${${var}}" PARENT_SCOPE)
    endfunction()
    
    set(txt "Hello" "There" "World")
    stringify_cr(multiline ${txt})
    message(${multiline})
    

If you don't like the additional newline at the end add string(STRIP "${multiline}" multiline).

like image 24
Florian Avatar answered Jan 17 '23 15:01

Florian