[dbohdan] 2014-06-14: I wanted to have a multiline string help message indented to the level of the rest of the code. The following procedure unindents it appropriately but keeps the relative indentation. ====== # Trim indentation in multiline quoted text based on the first line's. proc trim-indentation {msg} { set msgLines [split $msg \n] if {[lindex $msgLines 0] eq {}} { shift msgLines } set firstLine [lindex $msgLines 0] set indent [ expr { [string length $firstLine] - [string length [string trimleft $firstLine]] } ] return [ join [ struct::list mapfor x $msgLines {string range $x $indent end} ] \n ] } ====== ***Use example*** ====== [...] if {[llength $args] == 0} { # Print help puts -nonewline [ trim-indentation { Command pipelines for interactive programming. usage: |> cmd1 |> cmd2 _ |> cmd3 #0 ?-debug? or |> { cmd1 |> cmd2 $_ |> cmd3 $pipe(0) } ?-debug? See http://wiki.tcl.tk/17419 for more. } return ] # End help } [...] ====== ***Output*** ====== Command pipelines for interactive programming. usage: |> cmd1 |> cmd2 _ |> cmd3 #0 ?-debug? or |> { cmd1 |> cmd2 $_ |> cmd3 $pipe(0) } ?-debug? See http://wiki.tcl.tk/17419 for more. ====== [PYK] 2014-06-14: What does `shift` do? This implementation uses the simplistic method of counting the number of characters trimmed from the first line, and fails for cases where the first line is more indented than other lines. To do the job right, see [textutil%|%textutil::undent]. Also, as of [Tcl 8.6%|%8.6], `[lmap]` is the [Tcl Commands%|%built-in] replacement for `[struct::list] mapfor`. [dbohdan]: The `shift` (an [eltclsh] built-in) is there because when the command is used like in the example above and the argument is split at newlines the first line becomes `{}`. I wasn't aware of `textutil::undent`; thanks for suggesting it! Out of curiosity I implemented trimming by the longest possible unindent in a [functional programming%|%functional style]. Its advantage over `textutil::undent` is that it supports custom and multiple delimiters, although when multiple delimiters are specified it treats them as interchangeable. `shift`ing is replaced with a proc called [List trim%|%ltrim] that removes empty items on both ends of a list. It is called to get rid of empty lines and lines of just the base level of indentation preceding and following the text, which I found useful for inline documents like the help message example above. ====== # Trim indentation in multiline quoted text. proc trim-indentation {msg {whitespaceChars " "}} { set msgLines [split $msg "\n"] set maxLength [string length $msg] set regExp [subst -nocommands {([$whitespaceChars]*)[^$whitespaceChars]}] set indent [ tcl::mathfunc::min {*}[ struct::list mapfor x $msgLines { if {[regexp $regExp $x match whitespace]} { string length $whitespace } else { lindex $maxLength } } ] ] return [ join [ ltrim [ struct::list mapfor x $msgLines {string range $x $indent end} ] ] "\n" ] } # Remove empty items at the beginning and the end of a list. proc ltrim {list} { set first [lsearch -not -exact $list {}] set last [lsearch -not -exact [lreverse $list] {}] return [ if {$first == -1} { list } else { lrange $list $first end-$last } ] } ====== (I'm not using `lmap` here because I'm on 8.5.) ====== eltclsh > trim-indentation "\n\n\n a\n\n b\n\n\n" a b eltclsh > trim-indentation "\n\n\n a\n\n b\n \n\n\n" a b eltclsh > trim-indentation "\n\n\n a\n\n b\n \n\n\n" a b eltclsh > trim-indentation "-----a\n---b\n-----c\n" - --a b --c ====== ---- [PYK] 2017-06-04: `trim-indentation` may munge data when the common indentation is not found on the first line. `[ycl%|%ycl string dedent]` handles this case. <>String Processing