447

I have a variable var in a Bash script holding a string:

echo $var
"some string.rtf"

I want to remove the last four characters of this string and assign the result to a new variable var2, so that

echo $var2
"some string"

How can I do this?

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
a06e
  • 18,594
  • 33
  • 93
  • 169

12 Answers12

416

You can do like this (in bash v4 and higher):

#!/bin/bash

v="some string.rtf"

v2=${v::-4}

echo "$v --> $v2"

Note: macos uses bash 3.x by default

Tom Harrison
  • 13,533
  • 3
  • 49
  • 77
onur
  • 5,647
  • 3
  • 23
  • 46
  • 94
    It is `bash` 4+, but in earlier version you can use the slightly longer `${v::${#v}-4}`. – chepner Dec 26 '14 at 18:54
  • 17
    Didn't worked for me, bash says '' Bad substitution' – Ivan Marjanovic Oct 28 '16 at 15:50
  • 35
    for some reason, in zsh `${v::-4}` croaks "zsh: closing brace expected". But @fredtantini 's answer below `${v:0:-4}` works fine. – Pierre D Jan 28 '17 at 14:19
  • 1
    @IvanMarjanovic "Didn't worked for me, bash says '' Bad substitution' "........Did you got any solution for it ? – Ashish Karpe Aug 30 '17 at 05:09
  • 1
    @AshishKarpe For me Etan's solution worked. Change line 2 to v2=${v%.*} – Ivan Marjanovic Sep 01 '17 at 13:27
  • 20
    Error with: -2: substring expression < 0 – Edward Oct 24 '18 at 15:28
  • 3
    @PierreD day saver, I searched for the closing brace problem first in the wrong direction and tried to escape the braces. But zsh just needs a start index. This also explains the two double colons. Thanks anishane in your comment to fredtantinis answer. – Timo Nov 04 '20 at 11:39
  • 3
    @Edward I just hit this issue too, in my case it seems to be because macOS doesn't support the negative shorthand in substrings (https://superuser.com/questions/1033273/bash-4-3-substring-negative-length-on-os-x). The solution from there works but is ugly: `v2=${v:0:$((${#v} - 4))}` – Toastrackenigma Feb 08 '21 at 20:59
  • For some reason, i got bad substitution if i run the script as root but if i just add root to the line on the script that requires root the bad substitution error goes away. leaving the comment it might help someone. – Chop Labalagun Aug 20 '21 at 22:04
  • how do I make this into a one liner? `export HOSTNAME = hostname echo "/lfs/${HOSTNAME::-13}/0/brando9"` – Charlie Parker Nov 12 '22 at 00:53
309

To remove four characters from the end of the string use ${var%????}.

To remove everything after and including the final . use ${var%.*}.

See Bash's documentation on parameter expansion for more.

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Etan Reisner
  • 77,877
  • 8
  • 106
  • 148
  • 22
    Pure shell answer, and will work in BASH 3.x that's found on many systems that have refused to implement BASH 4.x due to licensing issues. – David W. Dec 26 '14 at 15:23
  • 1
    This is cool since it's robust against getting shorter strings; the index offset variants fail then. – Raphael Apr 26 '17 at 09:19
  • works in bash 3.2.25 - the best possible answer - just what I was looking for – capser Oct 25 '17 at 22:26
  • Excellent answer. How about a removal at the front of the string? – user2023370 Jan 24 '19 at 15:38
  • 4
    @user2023370 Consulting the documentation should reveal that there is a corresponding parameter substitution `${var#????}` for that. – tripleee Jul 14 '19 at 10:16
  • how to code this if it's in a for loop where i'th places are removed? (e.g.) input: `abcde`, 1st iteration output: `abcde`, 2nd iteration output: `abcd`, 3rd iteration output: `abc`) – dcdum2018 Oct 29 '21 at 02:27
  • why doesn't `echo "/lfs/$({hostname::-13})/0/brando9" ` work? – Charlie Parker Nov 12 '22 at 00:51
  • how do I make this into a one liner? `export HOSTNAME = hostname echo "/lfs/${HOSTNAME::-13}/0/brando9"` – Charlie Parker Nov 12 '22 at 00:53
148

First, it's usually better to be explicit about your intent. So if you know the string ends in a .rtf that you want to remove, you can just use var2=${var%.rtf}. One potentially-useful aspect of this approach is that if the string doesn't end in .rtf, it is not changed at all; var2 will contain an unmodified copy of var.

If you want to remove a filename suffix but don't know or care exactly what it is, you can use var2=${var%.*} to remove everything starting with the last .. Or, if you only want to keep everything up to but not including the first ., you can use var2=${var%%.*}. Those options have the same result if there's only one . in the string, but if there might be more than one, you get to pick which end of the string to work from. On the other hand, if there's no . in the string at all, var2 will again be an unchanged copy of var.

If you really want to always remove a specific number of characters, here are some options.

You tagged this bash specifically, so we'll start with bash builtins. The one which has worked the longest is the same suffix-removal syntax I used above: to remove four characters, use var2=${var%????}. Or to remove four characters only if the first one is a dot, use var2=${var%.???}, which is like var2=${var%.*} but only removes the suffix if the part after the dot is exactly three characters. As you can see, to count characters this way, you need one question mark per unknown character removed, so this approach gets unwieldy for larger substring lengths.

An option in newer shell versions is substring extraction: var2=${var:0:${#var}-4}. Here you can put any number in place of the 4 to remove a different number of characters. The ${#var} is replaced by the length of the string, so this is actually asking to extract and keep (length - 4) characters starting with the first one (at index 0). With this approach, you lose the option to make the change only if the string matches a pattern. As long as the string has at least four characters, no matter what its actual value is, the copy will include all but its last four characters.

You can leave the start index out; it defaults to 0, so you can shorten that to just var2=${var::${#var}-4}. In fact, newer versions of bash (specifically 4+, which means the one that ships with MacOS won't work) recognize negative lengths as the index of the character to stop at, counting back from the end of the string. So in those versions you can get rid of the string-length expression, too: var2=${var::-4}. This interpretation is also triggered if you leave the string length in but the string is shorter than four characters, since then ${#var}-4 is negative. For example, if the string has three characters, ${var:0:${#var}-4} becomes ${var:0:-1} and removes only the last character.

If you're not actually using bash but some other POSIX-type shell, the pattern-based suffix removal with % will still work – even in plain old dash, where the index-based substring extraction won't. Ksh and zsh do both support substring extraction, but require the explicit 0 start index; zsh also supports the negative end index, while ksh requires the length expression. Note that zsh, which indexes arrays starting at 1, nonetheless indexes strings starting at 0 if you use this bash-compatible syntax. But zsh also allows you to treat scalar parameters as if they were arrays of characters, in which case the substring syntax uses a 1-based count and places the start and (inclusive) end positions in brackets separated by commas: var2=$var[1,-5].

Instead of using built-in shell parameter expansion, you can of course run some utility program to modify the string and capture its output with command substitution. There are several commands that will work; one is var2=$(sed 's/.\{4\}$//' <<<"$var").

Mark Reed
  • 91,912
  • 16
  • 138
  • 175
  • The ???? is a double edged sword, but it worked perfectly for opening a .git repo to the /branches folder. __repo=https://github.com/me/something.git # REM now go check __repo branches. ???? to remove the .git open "${__repo%????}/branches" – granadaCoder Sep 18 '20 at 14:54
  • Glad to have helped, but it would probably be clearer to be more explicit and use `${__repo%.git}`, since you know exactly which four characters you're removing. That also won't remove anything if the string _doesn't_ end in `.git`, so works even if the value of `__repo` doesn't include the suffix for some reason. – Mark Reed Sep 18 '20 at 19:19
  • 1
    @mark reed `cut -c -4` doesn't do what you think at all. It only prints from the first to 4th character from the string, which here would simply be `some`. You cannot achieve this with `cut`. – Atralb Feb 13 '21 at 22:01
  • Thanks, @Atralb. I must have tested with a string that happened to be 8 characters long. Updated. – Mark Reed Feb 13 '21 at 22:26
  • how do I make this into a one liner? `export HOSTNAME = hostname echo "/lfs/${HOSTNAME::-13}/0/brando9"` – Charlie Parker Nov 12 '22 at 00:53
  • @CharlieParker you can't do parameter expansion on a value without first assigning it to a parameter, and you can't combine operations like substring removal and search/replace in the same expansion. So you're looking at three commands, or else a pipeline using external utilities instead of parameter expansion. – Mark Reed Jan 05 '23 at 17:38
125

What worked for me was:

echo "hello world" | rev | cut -c5- | rev
# hello w

But I used it to trim lines in a file so that's why it looks awkward. The real use was:

cat somefile | rev | cut -c5- | rev

cut only gets you as far as trimming from some starting position, which is bad if you need variable length rows. So this solution reverses (rev) the string and now we relate to its ending position, then uses cut as mentioned, and reverses (again, rev) it back to its original order.

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Reut Sharabani
  • 30,449
  • 6
  • 70
  • 88
  • 1
    This is not correct and not an answer to what is being asked. `rev` reverses lines, not strings. `echo $SOME_VAR | rev | ...`will probably not behave how one would expect. – Mohammad Nasirifar Jun 20 '19 at 16:27
  • 2
    @Mohammad Because of the [broken quoting,](/questions/10067266/when-to-wrap-quotes-around-a-shell-variable) that *is* a single line. – tripleee Jul 14 '19 at 10:18
  • I think you can simplify this: `echo "hello world" | cut -c-7` and `echo "hello world" | rev | cut -c5- | rev` produce the same output. – Matthias Braun Aug 30 '20 at 07:27
  • For me it worked fine - both in a pipeline and in a variable as something like `var2=$(echo $var | rev | cut -c5- | rev)` but yes, technically this one works on lines. Might not work as expected for multiline if you don't want to process each line as separate string. – Zbyszek Feb 26 '21 at 20:15
  • @MohammadNasirifar you wrote "rev reverses lines, not strings" <-- putting aside that lines are strings. `echo -e 'abc\ndef' | rev` shows rev reverses each line, so therefore, if there is only one line it reverses just that. And if it is given just characters with no \n, e.g. `echo -n abc|rev`, it still reverses it. So is your issue that if the string has a \n in there it'd treat it as a new line and reverse between the \n chars and not across them? the question's examples didn't have new line characters. Did you have in mind `cat file | rev` that is no longer in their answer? – barlop May 31 '22 at 02:45
  • @tripleee where is their quoting broken? – barlop May 31 '22 at 02:53
  • @barlop `echo $SOME_VAR` in the first comment should be `echo "$SOME_VAR"` – tripleee May 31 '22 at 04:27
49

Using Variable expansion/Substring replacement:

${var/%Pattern/Replacement}

If suffix of var matches Pattern, then substitute Replacement for Pattern.

So you can do:

~$ echo ${var/%????/}
some string

Alternatively,

If you have always the same 4 letters

~$ echo ${var/.rtf/}
some string

If it's always ending in .xyz:

~$ echo ${var%.*}
some string

You can also use the length of the string:

~$ len=${#var}
~$ echo ${var::len-4}
some string

or simply echo ${var::-4}

Community
  • 1
  • 1
fredtantini
  • 15,966
  • 8
  • 49
  • 55
39

You could use sed,

sed 's/.\{4\}$//' <<< "$var"

EXample:

$ var="some string.rtf"
$ var1=$(sed 's/.\{4\}$//' <<< "$var")
$ echo $var1
some string
Avinash Raj
  • 172,303
  • 28
  • 230
  • 274
14

This also can do the job:

... | head -c -1
-c, --bytes=[-]NUM
              print the first NUM bytes of each file; with the leading '-', print all but the last NUM bytes of each file
Stepan Seliuk
  • 179
  • 1
  • 4
  • 1
    `echo "$v" | head -c -1` results in "head: illegal byte count -- -1" with BSD head. Using GNU head, (ghead), `echo "$v" | ghead -c -1` results in "some string.rtf%"; no characters removed. `ghead -c -1 <<<'some string.rtf'` also results in no characters removed. Finally, to confirm there are no shenanigans.. `wc -c <<<"$v"; wc -c <<<"$(ghead -c -1 <<<"$v")"` shows that both have 16 bytes. You must use `head/ghead -c -2`. For those interested, `ghead --version` produces "head (GNU coreutils) 9.0" – SgtPooki Nov 05 '21 at 21:54
  • @SgtPooki that error might be caused by a multi-byte character, and your locale matters. – ryenus Feb 28 '23 at 09:14
8

Hope the below example will help,

echo ${name:0:$((${#name}-10))} --> ${name:start:len}

  • In above command, name is the variable.
  • start is the string starting point
  • len is the length of string that has to be removed.

Example:

    read -p "Enter:" name
    echo ${name:0:$((${#name}-10))}

Output:

    Enter:Siddharth Murugan
    Siddhar

Note: Bash 4.2 added support for negative substring

  • This is much more verbose than a Bourne-compatible simple pattern substitution as in Etan Reisner's answer. – tripleee Jul 14 '19 at 10:21
  • 1
    I also had to use `echo "{${name:0:${#name} - 1}}"` to remove the last char and to avoid "substring expression < 0" errors. – Clemens Jul 11 '20 at 05:20
  • This answer is simple to understand and is similar to Python's syntax. Good. – GoingMyWay Aug 26 '21 at 02:53
8

In this case you could use basename assuming you have the same suffix on the files you want to remove.

Example:

basename -s .rtf "some string.rtf"

This will return "some string"

If you don't know the suffix, and want it to remove everything after and including the last dot:

f=file.whateverthisis
basename "${f%.*}"

outputs "file"

% means chop, . is what you are chopping, * is wildcard

Gigi
  • 347
  • 4
  • 11
7

I tried the following and it worked for me:

#! /bin/bash

var="hello.c"
length=${#var}
endindex=$(expr $length - 4)
echo ${var:0:$endindex}

Output: hel

Priya Jain
  • 795
  • 5
  • 15
  • This is perfect. I've been looking for this exact answer for quite some time. This is the closest code I've found so far to python's iterable slicing functionality. – VanBantam Jul 09 '20 at 19:00
6

The top answer doesn't work for me, because mac os x ships with a different version of bash.

I use sed like so:

var2=`echo $var2 | sed 's/.$//'`

removes the last character

var2=`echo $var2 | sed 's/..$//'`

removes the last 2 characters.

Victor Pudeyev
  • 4,296
  • 6
  • 41
  • 67
4

This worked for me by calculating size of string.
It is easy you need to echo the value you need to return and then store it like below

removechars(){
        var="some string.rtf"
        size=${#var}
        echo ${var:0:size-4}  
    }
    removechars
    var2=$?

some string

Saurabh
  • 7,525
  • 4
  • 45
  • 46