2

I was trying to do these few operations/commands on a single line and assign it to a variable. I have it working about 90% of the way except for one part of it.

I was unaware you could do this, but I read that you can nest $(..) inside other $(..).... So I was trying to do that to get this working, but can't seem to get it the rest of the way.

So basically, what I want to do is:
1. Take the output of a file and assign it to a variable
2. Then pre-pend some text to the start of that output
3. Then append some text to the end of the output
4. And finally remove newlines and replace them with "\n" character...

I can do this just fine in multiple steps but I would like to try and get this working this way.

So far I have tried the following:

My 1st attempt, before reading about nested $(..):

MY_VAR=$(echo -n "<pre style=\"display:inline;\">"; cat input.txt | sed ':a;N;$!ba;s/\n/\\n/g'; echo -n "</pre>")

This one worked 99% of the way except there was a newline being added between the cat command's output and the last echo command. I'm guessing this is from the cat command since sed removed all newlines except for that one, maybe...?

Other tries:

MY_VAR=$( $(echo -n "<pre style=\"display:inline;\">"; cat input.txt; echo -n "</pre>") | sed ':a;N;$!ba;s/\n/\\n/g')

MY_VAR="$( echo $(echo -n "<pre style=\"display:inline;\">"; cat input.txt; echo "</pre>") | sed ':a;N;$!ba;s/\n/\\n/g' )"

MY_VAR="$( echo "$(echo -n "<pre style=\"display:inline;\">"; cat input.txt; echo "</pre>")" | sed ':a;N;$!ba;s/\n/\\n/g' )"

*Most these others were tried with and without the extra double-quotes surrounding the different $(..) parts...

I had a few other attempts, but they didn't have any luck either... On a few of the other attempts above, it seemed to work except sed was NOT inserting the replacement part of it. The output was correct for the most part, except instead of seeing "\n" between lines it just showed each of the lines smashed together into one line without anything to separate them...

I'm thinking there is something small I am missing here if anyone has any idea..?

*P.S. Does Bash have a name for the $(..) structure? It's hard trying to Google for that since it doesn't really search symbols...

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Matt
  • 413
  • 1
  • 10
  • 16
  • 1
    `$(..)` is called *command-substitution*. – David C. Rankin Feb 11 '16 at 21:37
  • Does your input.txt have a trailing newline? If you want its contents to all be on one line, perhaps you want `tr -d '\n' – Charles Duffy Feb 11 '16 at 21:37
  • 1
    BTW, `echo -n` is depending on behavior POSIX doesn't specify (literally, the POSIX spec lets `echo` do *literally anything* when passed the `-n` argument). Better to use `printf '%s' 'blahblahblah'`. – Charles Duffy Feb 11 '16 at 21:37
  • Instead of $(...) the nexted one could use \`...\` – Jerry Jeremiah Feb 11 '16 at 21:43
  • ...though perhaps you want to remove only the *last* newline in `input.txt`? – Charles Duffy Feb 11 '16 at 21:46
  • Aside: All-caps variable names are reserved for variables with meaning to the system or the shell. See POSIX spec at http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html, fourth paragraph, keeping in mind that environment variables and shell variables share a namespace. – Charles Duffy Feb 11 '16 at 21:50
  • @JerryJeremiah, `$()` doesn't require any tricks to nest cleanly -- that being one of its prime advantages over the backtick syntax to which it's a preferred replacement. [The OP's question here is not really revolving around nesting command substitutions at all, but around their desire to strip a trailing newline from a file being streamed with `cat`, misleading title notwithstanding]. – Charles Duffy Feb 11 '16 at 21:52
  • Hey guys, thanks for the replies. Wow lots of replies here, many thanks. Maybe I was just trying to be a little too cute in my code syntax... I never tried nesting $() before, and had found this post here: http://stackoverflow.com/questions/17984958/what-does-it-mean-in-shell-when-we-put-a-command-inside-dollar-sign-and-parenthe which seemed pretty close to what I was attempting, so maybe I got a littel too fixated on that method... But, yea I guess the main goal was to remove that trailing newline from cat, which I have seen a bunch of examples of now in the comments to this post. So thanks! – Matt Feb 11 '16 at 22:33
  • Also, apologies for the misleading title, was in a heck of a rush when I was posting this and that was the best i could come up with at that particular moment... As far as using tr, I had originally attempted to use tr until I relaized that tr only did a char for char replacement, so trying to remove '\n' and then inserting a '\\n' was throwing an error. So I thought it would be better/easier to use one command, *sed*, instead of using *sed* and *tr*... But alas, that seems like the better way to go as posted by Charles. – Matt Feb 11 '16 at 22:41

1 Answers1

4

You have no need to nest command substitutions here.

your_var='<pre style="display:inline;">'"$(<input.txt)"'</pre>'
your_var=${your_var//$'\n'/'\n'}

  • "$(<input.txt)" expands to the contents of input.txt, but without any trailing newline. (Command substitution always strips trailing newlines; printf '%s' "$(cat ...)" has the same effect, albeit less efficiently as it requires a subshell, whereas cat ... alone does not).
  • ${foo//bar/baz} expands to the contents of the shell variable named foo, with all instances of bar replaced with baz.
  • $'\n' is bash syntax for a literal newline.
  • '\n' is bash syntax for a two-character string, beginning with a backslash.
  • Thus, tying all this together, it first generates a single string with the prefix, the contents of the file, and the suffix; then replaces literal newlines inside that combined string with '\n' two-character sequences.

Granted, this is multiple lines as implemented above -- but it's also much faster and more efficient than anything involving a command substitution.


However, if you really want a single, nested command substitution, you can do that:

your_var=$(printf '%s' '<pre style="display:inline;">' \
                       "$(sed '$ ! s/$/\\n/g' <input.txt | tr -d '\n')" \
                       '</pre>')
  • The printf %s combines its arguments without any delimiter between them
  • The sed operation adds a literal \n to the end of each line except the last
  • The tr -d '\n' operation removes literal newlines from the file

However, even this approach could be done more efficiently without the nesting:

printf -v your_var '%s' '<pre style="display:inline;">' \
                        "$(sed '$ ! s/$/\\n/g' <input.txt | tr -d '\n')" \
                        '</pre>')

...which has the printf assign its results directly to your_var, without any outer command substitution required (and thus saving the expense of the outer subshell).

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Now that's a much better way to do it. – Jerry Jeremiah Feb 11 '16 at 21:45
  • Thanks for the in depth answer Charles, appreciate the explanations as well... Yea, like I mentioned above I guess I got a little too fixated on the nesting and what not, but if you say this way is fast/more efficent/etc, then I'll just do that. – Matt Feb 11 '16 at 22:38
  • To be fair, the efficiency depends in part on the file size. bash's built-in string handling can be pretty bad with really long inputs, *eventually* getting bad enough to overtake the overhead of spinning up external tools. But when working with shorter strings, that overhead is large enough that bash's internal functionality takes no time at all in comparison. – Charles Duffy Feb 11 '16 at 22:39
  • The file only has 21 lines, and will always have that many lines, so file size shouldn't be an issue... Thanks again! – Matt Feb 11 '16 at 22:46
  • Hummm, maybe I had the answer all along.... Was just looking at some of my 1st few attempts at this, which I had commented out inside my script, and the first one I had commented out was this: `my_var=$(echo "
    ${my_var//$'\n'/\\n}
    ")` where the `$my_var` var contains the contents of the text file, and the output is the exact same as far as I can tell. And I had so many different attempts that I'm wondering why I went away from that. Maybe it was because I was trying to bypass a 2nd line where I set `my_var=$(cat input.txt)`... I know I know seems silly now...
    – Matt Feb 11 '16 at 23:00