6

I know that I can remove substrings from the front of a variable:

X=foobarbaz
echo ${X#foo}       # barbaz

...and from the back of a variable:

echo ${X%baz}       # foobar

How do I combine the two? I've tried:

echo ${{X#foo}%baz}    # "bad substitution"
echo ${${X#foo}%baz}   # "bad substitution"
echo ${X#foo%baz}      # foobarbaz

I don't think I can use an intermediate variable, because this is being used in find -exec, in something like the following:

find ./Source -name '*.src' \
    -exec bash -c 'myconvert -i "{}" -o "./Dest/${0#./Source/%.src}.dst"' {} \;
  • Could the inner brace be an alias ? I'm not so good with Bash aliases. – Arif Burhan Apr 2 '16 at 21:22
  • Note, before bash had such string manipulation we used expr (which uses an anchored regexp not globs). Eg expr "foobarbaz" : 'foo\(.*\)baz$' returns the captured "bar". – meuh Apr 3 '16 at 9:37
7

I don't think that's possible (but would love to be proved wrong). However, you can use an intermediate variable. The bash -c run by -exec is just a bash instance, like any other:

$ find . -type f
./foobarbaz
$ find . -type f -exec bash -c 'v=${0#./foo}; echo ${v%baz}'  {} \;
bar

So I see no reason why this wouldn't work (if I understood what you're trying to do correctly):

find ./Source -name '*.src' \
     -exec bash -c 'v=${0%.src}; \ 
        myconvert -i "{}" -o "./Dest/${v#./Source/}.dst"' {} \;
5

The usual way is to do it in two steps:

x=foobarbaz
y=${x#foo}       # barbaz
z=${y%baz}       # bar

As this are "Parameter Expansions", a "Parameter" (variable) is needed to be able to perform any substitution. That means that y and z are needed. Even if they could be the same variable:

x=foobarbaz
x=${x#foo}       # barbaz
x=${x%baz}       # bar

find ./Source/ -name '*.src' -exec \
    bash -c '
        x=${1#./Source/}
        myconvert -i "$1" -o "./Dest/${x%.src}.dst"
    ' shname {} \;

Where shname is parameter $0 and the next {} is parameter $1 to the bash call.


A simpler alternative is to do a cd ./Source at the begining to avoid removing that part later. Something like:

cd ./Source                 ### Maybe test that it exist before cd to it.
shopt -s extglob nullglob   ### Make sure that correct options are set.

for f in *.src **/*.src; do
    echo \
    myconvert -i "$f" -o "./Dest/${f%.src}.dst"
done

Once you are convinced that it does what you want, comment out the echo.

1

I always do it one step at a time:

X=foobarbaz; X="${X#foo}"; X="${X%baz}"
0

Another method, relative to the OP's concept, is to use sed.

For example, if you have a filename format like this:

/music/artist/album

And you wish to extract "artist", this will work:

filename="/music/artist/album"
sed -nr 's/.*\/(.*)\/.*/\1/p' <<< "$filename"

Returns:

artist

In the example above, sed isolates the middle sub-string by removing everything left and right of the first pattern bounded by a '/' character on either side. The delimiters are also removed.

Note that sed begins dissecting the string from right to left. Therefore, it doesn't matter if your filename path in this illustration begins with / or not. For instance, it returns the same result ("artist") for either of these paths:

/music/artist/album

music/artist/album

About the command line

-n limits the output to one iteration

-r allows the use of a regex expression /p tells sed to print the result

Also, you may prefer this slightly shorter version of the command (though it is less explicit):

sed -r 's/.*\/(.*)\/.*/\1/' <<< "$filename"

If you want to use a different character or string as boundary delimiters, change the character(s) in the position where you see the 'X' below:

sed -nr 's/.*\X(.*)\X.*/\1/p' <<< "$test"

..........................^.........^

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.