158

What is the simplest way to remove a trailing slash from each parameter in the '$@' array, so that rsync copies the directories by name?

rsync -a --exclude='*~' "$@" "$dir"

The title has been changed for clarification. To understand the comments and answer about multiple trailing slashes see the edit history.

mklement0
  • 382,024
  • 64
  • 607
  • 775
sid_com
  • 24,137
  • 26
  • 96
  • 187
  • 2
    possible duplicate of [Remove slash from the end of a variable](http://stackoverflow.com/questions/1848415/remove-slash-from-the-end-of-a-variable) – Warren Dew Sep 24 '14 at 08:31
  • Those who don't know, in bash `a//b` is same as `a/b`. So in some cases one don't have to remove the trailing slash. – Gayan Weerakutti Aug 03 '22 at 11:58

9 Answers9

240

You can use the ${parameter%word} expansion that is detailed here. Here is a simple test script that demonstrates the behavior:

#!/bin/bash

# Call this as:
#   ./test.sh one/ two/ three/ 
#
# Output:
#  one two three

echo ${@%/}
Sean Bright
  • 118,630
  • 17
  • 138
  • 146
  • 46
    +1: To be highly pedantic, that will remove a single slash, not all trailing slashes. To remove any number of trailing slashes: `shopt -s extglob; echo "${@%%+(/)}"` – glenn jackman Jan 26 '12 at 17:19
  • 28
    Warning: you may not want to remove trailing slashes in all cases. If "/" is supplied as an argument, removing the trailing slash will have ...unfortunate consequences. – Gordon Davisson Jan 26 '12 at 19:17
  • 14
    PROTIP: Combine `tr -s /` with variable regex to remove repeated slashes then remove trailing slash. e.g. `DIR=$(echo //some///badly/written///dir////// | tr -s /); DIR=${DIR%/}` – Dave Nov 14 '14 at 14:36
  • @Dave Why spawn an external `tr` when it can be done entirely within the shell, as shown by @glennjackman above? Never mind that the OP didn't ask to have strings of `/` characters squashed from earlier parts of the string... – twalberg Jan 08 '15 at 18:08
  • @twalberg tr removes ANY repeated slashes. glennjackman example only removes TRAILING slashes. Also, I'm not sure that it is POSIX compliant. My example is POSIX compliant and does more cleanup. It was just a related thought. Doesn't answer the OP question directly. – Dave Jan 09 '15 at 17:44
  • @Dave Exactly. The OP did not ask to remove ANY repeated slashes. Just trailing ones... – twalberg Jan 09 '15 at 18:28
  • @glennjackman looks nice, but doe not do ANYTHING. `string_in == string_out` – Julian F. Weinert Jan 22 '15 at 16:19
  • 1
    Really? At a bash prompt, run `set -- one///// two// three four/; shopt -s extglob; echo "${@%%+(/)}"` and tell me what you see – glenn jackman Jan 22 '15 at 16:46
  • 1
    @twalberg I appreciate "protips" that aren't just alternative answers to OP's question in comment threads. – Kyle Strand Feb 20 '15 at 20:12
  • I'd say a separate script is overkill, see @Ivan's answer. – einpoklum Aug 06 '16 at 08:56
  • @einpoklum, it's in a "separate script" so that OP (or anyone) can easily just copy/paste it to test locally. – Sean Bright Aug 06 '16 at 10:05
  • 1
    Worrying about whether "tr" is spawned as a subprocess indicates that shell is not the correct solution for a problem (unless one is answering a twisty question for the Computer Bowl). Reading clarity trumps everything in 2010+ (insofar as that can even be had in sh/bash, stuff's is badly entropic with backwards compatibility) – David Tonhofer Sep 16 '17 at 13:31
  • Nice and simple, BUT BEWARE: it breaks on arguments containing spaces! (at least if used in the way the OP intended). – mivk Oct 18 '20 at 15:33
  • rtFolderin=$1 rtFolder=${$rtFolderin%%+(/)} does not work for shell argument – mendel Feb 23 '21 at 02:21
61

The accepted answer will trim ONE trailing slash.

One way to trim multiple trailing slashes is like this:

VALUE=/looks/like/a/path///

TRIMMED=$(echo "$VALUE" | sed 's:/*$::')

echo "$VALUE" "$TRIMMED"

Which outputs:

/looks/like/a/path/// /looks/like/a/path
Chris Johnson
  • 20,650
  • 6
  • 81
  • 80
  • 1
    Don't forget to quote your variables, in case they contain whitespace: `TRIMMED=$(echo "$VALUE" | sed 's:/*$::')` – tetsujin Jul 13 '17 at 17:15
  • 3
    Actually that isn't necessary inside the `$()` construct. However it is also harmless :) so it's probably a good practice to use double quotes like `"$VALUE"` so you don't have to decide when to and when not to use the double quotes. – Chris Johnson Jul 13 '17 at 17:55
  • Any way to combine this with an earlier capture? I want to strip protocol and (if present) trailing slash from a URL but `echo "https://www.example.com/foo/" | sed -e 's|https*://\(.*\)/*$|\1|'` doesn't work (since the capture group matches the trailing slash as well i guess). I can do it with two commands: `echo "https://www.example.com/foo/" | sed -e 's|https*://\(.*\)$|\1|' -e 's|/*$||'` but wondered if it could be done with one? – Adam Aug 23 '17 at 12:15
  • This answer worked better for me with a very big input file. A simple `sed 's:/*$::' < in.txt > out.txt` does the job in seconds – MitchellK May 21 '19 at 07:21
39

This works for me: ${VAR%%+(/)}

As described here http://wiki.bash-hackers.org/syntax/pattern

May need to set the shell option extglob. I can't see it enabled for me but it still works

mklement0
  • 382,024
  • 64
  • 607
  • 775
Ivan
  • 391
  • 3
  • 2
  • 3
    To query a setting: `shopt extglob` with no options – glenn jackman Jan 22 '15 at 16:47
  • 4
    This is Extended Pattern Language and you **must** set `extglob`. – ingyhere Jan 06 '16 at 21:54
  • 2
    This doesn't work on Mac OS X's builtin bash. The solution by Sean Bright above does: `${VAR%/}` – Alec Jacobson Jan 06 '17 at 14:00
  • Pitfall coming from Perl and that realm of regex is that my instinct was to half forget working in globbing and pattern match, and instead try `${VAR%%/*}`, which deletes the first slash through the end (e.g. "foo///bar/foobar" becomes "foo"). *P.S. Surprisingly, extglob is on by default in Fedora 36.* – Kevin Nov 30 '22 at 16:19
36

realpath resolves given path. Among other things it also removes trailing slashes. Use -s to prevent following simlinks

DIR=/tmp/a///
echo $(realpath -s $DIR)
# output: /tmp/a
czerny
  • 15,090
  • 14
  • 68
  • 96
  • 3
    It requires all nodes in the path except for the last one to exist. If the user throws in some non-existence path, `realpath` shall fail. – Livy Aug 31 '19 at 02:12
  • 3
    @Livy `realpath --canonicalize-missing` works absolutely correctly with any non-existing part of path – maoizm Mar 09 '20 at 17:33
  • and realpath is missing on some platforms :-( – Mark Nov 02 '20 at 21:54
11

FYI, I added these two functions to my .bash_profile based on the answers found on SO. As Chris Johnson said, all answers using ${x%/} remove only one slash, these functions will do what they say, hope this is useful.

rem_trailing_slash() {
    echo "$1" | sed 's/\/*$//g'
}

force_trailing_slash() {
    echo "$(rem_trailing_slash "$1")/"
}
Jonathan H
  • 7,591
  • 5
  • 47
  • 80
6

In zsh you can use the :a modifier.

export DIRECTORY='/some//path/name//'

echo "${DIRECTORY:a}"

=> /some/path/name

This acts like realpath but doesn't fail with missing files/directories as argument.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Nicolai Fröhlich
  • 51,330
  • 11
  • 126
  • 130
1

Approach I have used, when trimming directory arguments that are intended for rsync, here using dirname and basename to split the path and then recombining the parts without the trailing slash.

raw_dir=/a/b/c/
trim_dir=$(dirname "$raw_dir")"/"$(basename "$raw_dir")
Darren Smith
  • 2,261
  • 16
  • 16
0

Taking note of a couple comments in the accepted answer:

  1. Replace all repeated slashes //[...] with a single slash / (per @Dave comment)
  2. Remove trailing slash unless it is also the leading slash (i.e., the root filepath /) (per @GordonDavisson comment)
trimSlash() { for s; do sed -E 's://*:/:g; s:(^/)?/*$:\1:' <<< "${s}"; done; }

Not as concise as the answers using parameter substitution, but I think its worth the diligence.

Some test cases:

$ trimSlash "/" "///" "a/" "a/a/" "a///a/" "a/a" "a///a" "a///" "/a/a/" "///a///"
/
/
a
a/a
a/a
a/a
a/a
a
/a/a
/a
ardnew
  • 2,028
  • 20
  • 29
-1

Not the most beautiful way, but quick and easy.

I just add a slash and remove all doubles. Assuming such a pattern will not be found elsewhere.

WORD="abc/"
WORD=$WORD'/'
WORD=`echo $WORD | sed s/'\/\/'/''/g`
echo $WORD
Xavier Prudent
  • 1,570
  • 3
  • 25
  • 54