15

I would like to use variable substitution on a particular string that I access via a command. For example, if I copy something into my clipboard, I can access it like this.

$ xclip -o -selection clipboard
Here's a string I just copied.

If I assign it to a variable, then I can do variable substitution on it.

$ var=$(xclip -o -selection clipboard)
$ echo $var
Here's a string I just copied.
$ echo ${var/copi/knott}
Here's a string I just knotted.

However, is there a way to do variable substitution without assigning it to a variable? Conceptually, something like this.

$ echo ${$(xclip -o -selection clipboard)/copi/knott}
bash: ${$(xclip -o -selection clipboard)/copi/knott}: bad substitution

This syntax fails, because var should be a variable name, not a string.

5 Answers 5

13

No, you can't. bash and most other shells (except zsh) don't allow nested substitution.

With zsh, you can do nested substitution:

$ echo ${$(echo 123)/123/456}   
456
1
  • 2
    I'll accept this answer, since it provides some circumstantial evidence that it's not possible in bash. (And pushes me again towards migrating to zsh.)
    – Sparhawk
    Commented Mar 12, 2015 at 1:13
5

Yeah, you can do that - kind of. It's really not pretty. It's more like in-line than nested. The problem is you have to operate on the value of the parameter you expand - if that parameter has no value then you won't do much. So, you can assign the value while expanding it and it's hardly a shortcut.

v=; echo "${v:=${0##*["$0${v:=$(xsel -bo)}"]}${v/copi/knott}}"

I use the $0 param expansion within the chain to hide the assignment. It assigns the var's value within a nested assignment expansion. The outer takes precedence - but because it would just expand to whatever the inner one does it's hard to tell. However, if we silence the inner expansion, then modify it you can get what you want. After copying your string to my clipboard (I don't have xclip - just xsel) it prints:

Here's a string I just knotted.

It's a little clearer what's going on if you leave $0 out, though:

v=; echo "${v:=${v:=$(xsel -bo)}${v/copi/knott}}"

That prints:

Here's a string I just copied.  Here's a string I just knotted.

...because the inner assignment occurs before the modification, but, as noted, the outer assignment takes precedence - and it expands to both the inner-assignment's expansion and to the modified inner-expansion.

Of course none of that works at all if the targeted parameter is already assigned - so you can only do it surely if you empty the variable in the first place... which, honestly, is probably the most convenient time to assign it after all.

3
  • +1 for the workaround, although as you say, it's a workaround that probably worse than assigning a variable!
    – Sparhawk
    Commented Mar 12, 2015 at 1:11
  • @Sparhawk - yeah, definitely worse. And there's really nothing wrong w/ that anyway - there's not much to gain except uncertainty. You could come up with some alias indirection to make it a little more convenient - but if its worth it to you then you should setup a function for handling safe-quoting and doing something w/ eval or something like it anyway. w/ eval - if you can make the first chars of the command sub's output amount to workable expansion syntax - then you can probably get a lot further a lot easier. I know such a thing would be easy w/ xsel - it takes stdin - but xsel?
    – mikeserv
    Commented Mar 12, 2015 at 2:19
  • @Sparhawk - I only know how to do any of that, by the way, because in some situations it can be useful - such as prompt or here-doc expansions - in which you can't get a current shell assignment to apply otherwise.
    – mikeserv
    Commented Mar 12, 2015 at 2:23
2

Here's an examplary hack, that bypasses assigning a temporary variable for chained parameter substitution:

declare -a values=( "4: " "#1" "#2" "#3" "#4" )
declare -A arr["VALUES"]="${values[@]}"

echo -e "Original: ${arr[@]}"

"${arr[@]#*: ?}" >&/dev/null; san="${_//#/}"

echo -e "Sanitized: $san"

Explanation:

We initialize an associative array arr with values from array values. The first element contains the number of value elements, followed by a :; this needs to be filtered out, as well as the prefix # for all values.

"${arr[@]#*: ?}" >&/dev/null

This is a parameter substitution, that first expands the values of arr and filters everything in front, up to and including '4: ' (#*: ?), discarding the result, both of stdout and stderr. This also suppresses an error, too, due to the missing variable assigning of the result.

>& is a short form of 2>&1, assigning the stderr output (fd2) to stdout (fd1) and both to /dev/null.

The trick now is, to use bash's special underscore _ parameter, which copies the discarded result parameter from the last command (1st parameter expansion operation above and result redirection to /dev/null) into our second parameter substitution, where the # value prefixes are filtered:

san="${_//#/}"

Output:

Original: 4: #1 #2 #3 #4

Sanitized: 1 2 3 4

Also see: Special parameters and shell variables

1

If you don't want to create a variable, then there are other ways to perform string substitution:

$ echo "$(xclip -o -selection clipboard | sed 's/copi/knott/')"
Here's a string I just knotted.
5
  • Thanks, I did know that I could use sed instead, but it was meant to be more of a general question, on nesting of substitutions.
    – Sparhawk
    Commented Mar 11, 2015 at 3:29
  • @Sparhawk To my knowledge, one cannot do variable substitution without having a variable.
    – John1024
    Commented Mar 11, 2015 at 3:32
  • Okay, that's probably the answer then. I'll leave the question open for a few days to see if anyone else has a referenced answer, then accept this one otherwise. Thanks.
    – Sparhawk
    Commented Mar 11, 2015 at 3:34
  • @Sparhawk Very good.
    – John1024
    Commented Mar 11, 2015 at 3:35
  • +1, but I'm going to accept another answer, since it provides slightly more concrete circumstantial evidence that it doesn't work in bash.
    – Sparhawk
    Commented Mar 12, 2015 at 1:12
0

On Linux, you can pipe into the source command:

echo $(xclip -o -selection clipboard)/copi/knott|source /dev/stdin

For systems that do not have /dev/stdin, you would have to write to a temporary file, then source it.

1
  • When I do this, I get “unexpected EOF” and “syntax error” errors. If I change the string to “Here is a string I just copied.” (i.e., remove the apostrophe), I get “bash: Here: command not found”.  How do you believe that this is supposed to work? Commented Jun 20, 2022 at 19:29

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .