Skip to content
VLSI Mentor

Tcl Part I · Module 5 · Strings

Trimming Whitespace

Removing the stray spaces around a value is the first thing nearly every line read from a file needs, and three subcommands do it. The basic use is obvious. The optional second argument is not, and it is the most dangerous misunderstanding in this module — because it looks like it removes a suffix, it does something quite different, and on the one filename most people test with it happens to give the right answer anyway. This chapter shows the damage first and the plausible case second, which is the order in which you want to meet them.

Foundation11 min readTclPart IStringsTrimmingParsing

Part I · Module 5 · Chapter 5.8 · Trimming Whitespace

1. The Common Case

Azvya Education Pvt. Ltd.VLSI Mentor
basic.tcl
set raw "   padded   "

puts "trim      -> <[string trim $raw]>"
puts "trimleft  -> <[string trimleft $raw]>"
puts "trimright -> <[string trimright $raw]>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
trim      -> <padded>
trimleft  -> <padded   >
trimright -> <   padded>

With one argument, all three remove whitespace — spaces, tabs, newlines — from the end or ends you asked for. The angle brackets in the output are mine, so you can see where the text stops.

This is the 90% use and it needs no further explanation.

2. Why Nearly Every Input Needs It

A line read from a file carries its line ending, and fields in real data carry padding:

Azvya Education Pvt. Ltd.VLSI Mentor
why.tcl
set field "  42  "

puts "untrimmed compare: [expr {$field eq "42"}]"
puts "trimmed compare:   [expr {[string trim $field] eq "42"}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
untrimmed compare: 0
trimmed compare:   1

The first comparison fails on invisible characters, which is exactly the kind of bug that takes an hour because the two values look identical when printed.

Pair it with case normalisation from Chapter 5.7 §5 and you have the standard boundary treatment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set key [string tolower [string trim $raw]]

3. The Second Argument Is Not a Suffix

Now the trap. All three accept a second argument, and it looks like it removes that text from the end:

Azvya Education Pvt. Ltd.VLSI Mentor
the-trap.tcl
puts "<[string trimright "report.txt" ".txt"]>"

Predict the output. Almost everyone expects report.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<repor>

repor. Two characters short.

Here is a second one:

Azvya Education Pvt. Ltd.VLSI Mentor
the-trap-2.tcl
puts "<[string trimright "extract.txt" ".txt"]>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<extrac>

And now the one that explains why this reaches production:

Azvya Education Pvt. Ltd.VLSI Mentor
the-trap-3.tcl
puts "<[string trimright "data.txt" ".txt"]>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<data>

That one is correct. The same wrong code, on a different filename, gives exactly the answer you wanted.

4. What It Actually Does

The second argument is a set of characters to remove, not a string to match.

".txt" is the set {. t x}. string trimright removes characters from the right-hand end for as long as each one is in that set:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
"report.txt"
  strip t  -> "report.tx"
  strip x  -> "report.t"
  strip t  -> "report."
  strip .  -> "report"
  strip t  -> "repor"     <- 't' is in the set, so it keeps going
  'o' is not in the set   -> stop

result: "repor"

It ate the t of report because t is in the set. data.txt survived because a is not in the set, so the stripping stopped at the right place by luck.

5. What to Use Instead

For a file extension, use file rootname:

Azvya Education Pvt. Ltd.VLSI Mentor
rootname.tcl
puts [file rootname "report.txt"]
puts [file rootname "extract.txt"]
puts [file rootname "archive.tar.gz"]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
report
extract
archive.tar

It understands what an extension is, and it handles the cases you did not think of.

For a genuine suffix, check and slice:

Azvya Education Pvt. Ltd.VLSI Mentor
suffix.tcl
proc strip_suffix {text suffix} {
    if {[string match "*$suffix" $text]} {
        return [string range $text 0 end-[string length $suffix]]
    }
    return $text
}

puts [strip_suffix "report.txt" ".txt"]
puts [strip_suffix "report.log" ".txt"]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
report
report.log

That uses Chapter 5.5 to confirm the suffix is there and Chapter 5.3 to cut it off, and it leaves the text alone when the suffix does not match.

6. When the Character Set Is What You Want

The feature is not a mistake — it is just not a suffix remover. It is right when you genuinely mean "strip any of these characters":

Azvya Education Pvt. Ltd.VLSI Mentor
character-set.tcl
puts "<[string trim "***heading***" "*"]>"
puts "<[string trim "  \t data \n " " \t\n"]>"
puts "<[string trimleft "000042" "0"]>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<heading>
<data>
<42>

Stripping decoration, stripping specific whitespace characters, stripping leading zeros — all genuine uses, and all cases where "any of these characters, repeatedly" is exactly the intent.

7. Common Mistakes

Using trimright to remove a suffix. §3. The headline bug of this module.

Testing the suffix bug on a filename where it happens to work. §3.

Forgetting to trim before comparing. §2.

Trimming a value you will display. Usually fine, but keep the original if exact text matters.

Assuming trimleft "000" "0" leaves a zero. It leaves nothing. §6.

8. Exercises

Exercise 1 — the three subcommands. On "\t padded \n", apply all three and print each result between angle brackets so you can see the boundaries.

Exercise 2 — reproduce the trap. Run string trimright with ".txt" on report.txt, extract.txt and data.txt. Explain, character by character, why the third one looks right.

Exercise 3 — find more victims. Name three other filenames that would be corrupted by string trimright $name ".txt", and one more that would survive. What property decides it?

Exercise 4 — do it correctly. Strip .txt from a filename two ways: with file rootname, and with the strip_suffix procedure from §5. Then test both on a file named notes.text.

Exercise 5 — a legitimate character set. Strip surrounding # characters from "###title###", and strip leading zeros from "00700". Then try your leading-zero code on "000" and fix what happens.

Exercise 6 — the boundary treatment. Write one line that takes a raw field and produces a trimmed, lowercased key. Test it with " Timeout \n".

9. Remember

  • With one argument, all three remove whitespace from the left, the right, or both.
  • Trim at the boundary, together with case normalisation, before comparing or using a value as a key.
  • The second argument is a SET OF CHARACTERS, not a suffix.
  • string trimright "report.txt" ".txt" gives repor — it keeps stripping while each character is in the set.
  • It looks correct on data.txt, which is how the bug survives testing.
  • Use file rootname for extensions, or match-then-slice for a genuine suffix.
  • The character-set behaviour is right for stripping decoration, specific whitespace, or leading zeros.
  • string trimleft "000" "0" is the empty string.

Next: changing text rather than removing it from the edges.

Where this fits

Part of the Tcl curriculum.