Fish shell - Interpolation in nested quotes - fish

I'm trying to write a fish function to display a notification after long commands have completed running. I have it working, but I'd like to know if there is a nicer way to get interpolation working with nested quotes.
function record_runtime --on-event fish_postexec
set text \"$argv took $CMD_DURATION\"
set command "display notification $text"
if [ $CMD_DURATION -gt 60000 ]
osascript -e "$command"
end
end
I was hoping for a one liner like osascript -e 'display notification "$argv took $CMD_DURATION"' but could not find a combination that worked.

So, what you want to do is execute osascript with one argument, that contains the command "display notification" and the value of $argv, the word "took" and the value of $CMD_DURATION. That means you want fish to expand these variables.
Importantly, fish does not expand variables in single-quotes (''), which is why your other attempt failed. Variables are only expanded in double-quotes or outside of quotes entirely.
Now I don't have a macOS machine to test, but if osascript also allows single quotes, this is simple:
function record_runtime --on-event fish_postexec
if [ $CMD_DURATION -gt 60000 ]
osascript -e "display notification '$argv took $CMD_DURATION'"
end
end
the single-quotes inside the double-quotes have no special meaning, so the $argv and $CMD_DURATION are expanded.
If osascript needs double-quotes here, you have to escape the inner double-quotes:
function record_runtime --on-event fish_postexec
if [ $CMD_DURATION -gt 60000 ]
osascript -e "display notification \"$argv took $CMD_DURATION\""
end
end

Related

ksh epoch to datetime format

I have a script for find the expire date of any user password. Script can find the expire date in seconds (epoch) but cannot convert this to datetime format.
#!/usr/bin/ksh
if (( ${#} < 1 )) ; then
print "No Arguments"
exit
fi
lastupdate=`pwdadm -q $1|awk '{print $3;}'`
((lastupdate=$lastupdate+0))
maxagestr=`lsuser -a maxage $1`
maxage=${maxagestr#*=}
let maxageseconds=$maxage*604800
expdateseconds=$(expr "$maxageseconds" + "$lastupdate")
((expdateseconds=$expdateseconds+0))
expdate=`perl -le 'print scalar(localtime($expdateseconds))'`
echo $expdateseconds
echo $expdate
In this script, expdateseconds value is true. If I type the value of expdateseconds as a parameter of localtime() function, the function show the date in datetime format.
But if I type the $expdateseconds variable, the function does not work true and return 01.01.1970 always.
How can I enter a variable as a parameter of localtime() function?
Shell variables are not expanded within single quotes. So in your code, perl is not "seeing" the shell variable, it is instead seeing an uninitialized perl variable whose value defaults to zero. Shell variables are expanded within double quotes, so in this case that's all you need to do:
expdate=`perl -le "print scalar(localtime($expdateseconds))"`
As #JeffY said, your problem is the quotes. You can also do it without perl (assuming your date command is the GNU version):
expdate=`date -d #$expdateseconds`
Although, since you're using ksh - and really, any modern POSIX shell - I recommend that you avoid `...`, which can cause confusing behavior with quoting, and use $(...) instead.
expdate=$(date -d #$expdateseconds)
This isn't codereview, but I have a few other tips regarding your script. The usual rule is to send error messages (like "No arguments") to standard error instead of standard out (with print -u2) and exit with a nonzero value (typically 1) when there's a usage error.
Whenever passing a parameter to a command, like pwadm -q $1, you run the risk of funny characters messing things up unless you double-quote the parameter: pwadm -q "$1".
You have an odd mixture of let, ((, and expr in your arithmetic. I would suggest that you declare all your numeric variables with typeset -ivarname and just use ((...)) for all arithmetic. Inside ((...)), you don't have to worry about globbing messing things up (let a=b*c will expand into a syntax error if you have a file in the current directory named a=b.c, for instance; (( a=b*c )) won't). You also don't need to put dollar signs on the variables (which just makes the shell convert them to a string and then parse their numeric value out again), or add 0 to them just to make sure they're numbers.
There is no need to use neither awk nor perl. E.g.:
#!/usr/bin/ksh93
[[ -z $1 ]] && print -u2 "No Arguments" && exit 1
typeset -i LASTUPDATE S
X=( ${ pwdadm -q "$1" ; } )
LASTUPDATE=${X[3]}
X=${ lsuser -a maxage "$1" ; }
S=${X#*=}
(( S *= 604800 ))
(( S += LASTUPDATE ))
printf "$S\n%T\n" "#$S"

powershell escape from >>

i'm just getting into PowerShell and i've encountered this problem:
if i place the escape character (') or single quotation mark (") at the end of a command PowerShell enters into a suspend/whatever mode where there are ">>" on the left on the screen, instead of "PS C:>", i know that PS just expects more information/commands/whatever from me and that is why it is providing me with new lines and what-not, but how do i escape/stop that? Even after pressing "Enter" a few times, PS just provides me with more >>s, i want to end my input and finish the command, and so far i was unable to find any information on that, mostly because i don't even know what's it called.
It's waiting for a closing quote or brace }, so you can either enter one of those and press enter twice more, or use Crtl+C to cancel the current command.
FYI:
' = literal quote (expects another single quote)
" = quotes to use when interpolating strings (expects another double quote)
` = escape character (continuation of the current line)
, = list separator (expects another list element)
{ = begin of script block (expects closing curly bracket)
As long as whatever you're doing is balanced, pressing enter twice will exit the block. You'd get another >> on the first press in case you wanted to start anything else, if not it will return you to the prompt.

Command line escaping single quote for PowerShell

I have a Windows application and on events, it calls a command like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x' -data '%y'"
The name parameter sometimes has ' in it. Is it possible to escape that somehow?
This is actually a lot trickier than you'd think. Escaping nested quotes in strings passed from cmd to PowerShell is a major headache. What makes this one especially tricky is that you need to make the replacement in a variable expanded by cmd in the quoted argument passed to powershell.exe within a single-quoted argument passed to a PowerShell script parameter. AFAIK cmd doesn't have any native functionality for even basic string replacements, so you need PowerShell to do the replacement for you.
If the argument to the -data paramater (the one contained in the cmd variable x) doesn't necessarily need to be single-quoted, the simplest thing to do is to double-quote it, so that single quotes within the value of x don't need to be escaped at all. I say "simplest", but even that is a little tricky. As Vasili Syrakis indicated, ^ is normally the escape character in cmd, but to escape double quotes within a (double-)quoted string, you need to use a \. So, you can write your batch command like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name \"%x%\" -data '%y%'"
That passes the following command to PowerShell:
G:\test.ps1 -name "value of x, which may contain 's" -data 'value of y'
If, however, x can also contain characters that are special characters in PowerShell interpolated strings (", $, or `), then it becomes a LOT more complicated. The problem is that %x is a cmd variable that gets expanded by cmd before PowerShell has a chance to touch it. If you single-quote it in the command you're passing to powershell.exe and it contains a single quote, then you're giving the PowerShell session a string that gets terminated early, so PowerShell doesn't have the opportunity to perform any operations on it. The following obviously doesn't work, because the -replace operator needs to be supplied a valid string before you can replace anything:
'foo'bar' -replace "'", "''"
On the other hand, if you double-quote it, then PowerShell interpolates the string before it performs any replacements on it, so if it contains any special characters, they're interpreted before they can be escaped by a replacement. I searched high and low for other ways to quote literal strings inline (something equivalent to perl's q//, in which nothing needs to be escaped but the delimiter of your choice), but there doesn't seem to be anything.
So, the only solution left is to use a here string, which requires a multi-line argument. That's tricky in a batch file, but it can be done:
setlocal EnableDelayedExpansion
set LF=^
set pscommand=G:\test.ps1 -name #'!LF!!x!!LF!'# -data '!y!'
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "!pscommand!"
This assumes that x and y were set earlier in the batch file. If your app can only send a single-line command to cmd, then you'll need to put the above into a batch file, adding the following two lines to the beginning:
set x=%~1
set y=%~2
Then invoke the batch file like this:
path\test.bat "%x%" "%y%"
The ~ strips out the quotes surrounding the command line arguments. You need the quotes in order to include spaces in the variables, but the quotes are also added to the variable value. Batch is stupid that way.
The two blank lines following set LF=^ are required.
That takes care of single quotes which also interpreting all other characters in the value of x literally, with one exception: double quotes. Unfortunately, if double quotes may be part of the value as you indicated in a comment, I don't believe that problem is surmountable without the use of a third party utility. The reason is that, as mentioned above, batch doesn't have a native way of performing string replacements, and the value of x is expanded by cmd before PowerShell ever sees it.
BTW...GOOD QUESTION!!
UPDATE:
In fact, it turns out that it is possible to perform static string replacements in cmd. Duncan added an answer that shows how to do that. It's a little confusing, so I'll elaborate on what's going on in Duncan's solution.
The idea is that %var:hot=cold% expands to the value of the variable var, with all occurrences of hot replaced with cold:
D:\Scratch\soscratch>set var=You're such a hot shot!
D:\Scratch\soscratch>echo %var%
You're such a hot shot!
D:\Scratch\soscratch>echo %var:hot=cold%
You're such a cold scold!
So, in the command (modified from Duncan's answer to align with the OP's example, for the sake of clarity):
powershell G:\test.ps1 -name '%x:'=''%' -data '%y:'=''%'
all occurrences of ' in the variables x and y are replaced with '', and the command expands to
powershell G:\test.ps1 -name 'a''b' -data 'c''d'
Let's break down the key element of that, '%x:'=''%':
The two 's at the beginning and the end are the explicit outer quotes being passed to PowerShell to quote the argument, i.e. the same single quotes that the OP had around %x
:'='' is the string replacement, indicating that ' should be replaced with ''
%x:'=''% expands to the value of the variable x with ' replaced by '', which is a''b
Therefore, the whole thing expands to 'a''b'
This solution escapes the single quotes in the variable value much more simply than my workaround above. However, the OP indicated in an update that the variable may also contain double quotes, and so far this solution still doesn't pass double quotes within x to PowerShell--those still get stripped out by cmd before PowerShell receives the command.
The good news is that with the cmd string replacement method, this becomes surmountable. Execute the following cmd commands after the initial value of x has already been set:
Replace ' with '', to escape the single quotes for PowerShell:
set x=%x:'=''%
Replace " with \", to escape the double quotes for cmd:
set x=%x:"=\"%
The order of these two assignments doesn't matter.
Now, the PowerShell script can be called using the syntax the OP was using in the first place (path to powershell.exe removed to fit it all on one line):
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x' -data '%y'"
Again, if the app can only send a one-line command to cmd, these three commands can be placed in a batch file, and the app can call the batch file and pass the variables as shown above (first bullet in my original answer).
One interesting point to note is that if the replacement of " with \" is done inline rather than with a separate set command, you don't escape the "s in the string replacement, even though they're inside a double-quoted string, i.e. like this:
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x:"=\"' -data '%y'"
...not like this:
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x:\"=\\"' -data '%y'"
I'm slightly unclear in the question whether %x and %y are CMD variables (in which case you should be using %x% to substitute it in, or a substitution happening in your other application.
You need to escape the single quote you are passing to PowerShell by doubling it in the CMD.EXE command line. You can do this by replacing any quotes in the variable with two single quotes.
For example:
C:\scripts>set X=a'b
C:\scripts>set Y=c'd
C:\scripts>powershell .\test.ps1 -name '%x:'=''%' '%y:'=''%'
Name is 'a'b'
Data is 'c'd'
where test.ps1 contains:
C:\scripts>type test.ps1
param($name,$data)
write-output "Name is '$name'"
write-output "Data is '$data'"
If the command line you gave is being generated in an external application, you should still be able to do this by assigning the string to a variable first and using & to separate the commands (be careful to avoid trailing spaces on the set command).
set X=a'b& powershell .\test.ps1 -name '%x:'=''%'
The CMD shell supports both a simple form of substitution, and a way to extract substrings when substituting variables. These only work when substituting in a variable, so if you want to do multiple substitutions at the same time, or substitute and substring extraction then you need to do one at a time setting variables with each step.
Environment variable substitution has been enhanced as follows:
%PATH:str1=str2%
would expand the PATH environment variable, substituting each occurrence
of "str1" in the expanded result with "str2". "str2" can be the empty
string to effectively delete all occurrences of "str1" from the expanded
output. "str1" can begin with an asterisk, in which case it will match
everything from the beginning of the expanded output to the first
occurrence of the remaining portion of str1.
May also specify substrings for an expansion.
%PATH:~10,5%
would expand the PATH environment variable, and then use only the 5
characters that begin at the 11th (offset 10) character of the expanded
result. If the length is not specified, then it defaults to the
remainder of the variable value. If either number (offset or length) is
negative, then the number used is the length of the environment variable
value added to the offset or length specified.
%PATH:~-10%
would extract the last 10 characters of the PATH variable.
%PATH:~0,-2%
would extract all but the last 2 characters of the PATH variable.
I beleive you can escape it with ^:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name ^'%x^' -data ^'%y^'"
Try encapsulating your random single quote variable inside a pair of double quotes to avoid the issue.
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name `"%x`" -data `"%y`""
The problem arises because you used single quotes and the random extra single quote appearing inside the single quotes fools PowerShell. This shouldn't occur if you double quote with backticks, as the single quote will not throw anything off inside double quotes and the backticks will allow you to double/double quote.
Just FYI, I ran into some trouble with a robocopy command in powershell and wanting to exclude a folder with a single quote in the name (and backquote didn't help and ps and robocopy don't work with double quote); so, I solved it by just making a variable for the folder with a quote in it:
$folder="John Doe's stuff"
robocopy c:\users\jd\desktop \\server\folder /mir /xd 'normal folder' $folder
One wacky way around this problem is to use echo in cmd to pipe your command to 'powershell -c "-" ' instead of using powershell "arguments"
for instance:
ECHO Write-Host "stuff'" | powershell -c "-"
output:
stuff'
Couple of notes:
Do not quote the command text you are echoing or it won't work
If you want to use any pipes in the PowerShell command they must be triple-escaped with carats to work properly
^^^|

How to capture single quote when using Perl in CLi?

Suppose I have a text file with content like below:
'Jack', is a boy
'Jenny', is a girl
...
...
...
I'd like to use perl in Cli to only capture the names between pairs of single quotes
cat text| perl -ne 'print $1."\n" if/\'(\w+?)\'/'
Above command was what I ran but it didn't work. It seems like "'" messed up with Shell.
I know we have other options like writing a perl script. But given my circumstances, I'd like to find a way to fulfill this in Shell command line.
Please advise.
The shell has the interesting property of concatenating quoted strings. Or rather, '...' or "..." should not be considered strings, but modifiers for available escapes. The '...'-surrounded parts of a command have no escapes available. Outside of '...', a single quote can be passed as \'. Together with the concatenating property, we can embed a single quote like
$ perl -E'say "'\''";'
'
into the -e code. The first ' exits the no-escape zone, \' is our single quote, and ' re-enters the escapeless zone. What perl saw was
perl // argv[0]
-Esay "'"; // argv[1]
This would make your command
cat text| perl -ne 'print $1."\n" if/'\''(\w+?)'\''/'
(quotes don't need escaping in regexes), or
cat text| perl -ne "print \$1.qq(\n) if/'(\w+?)'/"
(using double quotes to surround the command, but using qq// for double quoted strings and escaping the $ sigil to avoid shell variable interpolation).
Here are some methods that do not require manually escaping the perl statement:
(Disclaimer: I'm not sure how robust these are – they haven't been tested extensively)
Cat-in-the-bag technique
perl -ne "$(cat)" text
You will be prompted for input. To terminate cat, press Ctrl-D.
One shortcoming of this: The perl statement is not reusable. This is addressed by the variation:
$pline=$(cat)
perl -ne "$pline" text
The bash builtin, read
Multiple lines:
read -rd'^[' pline
Single line:
read -r pline
Reads user input into the variable pline.
The meaning of the switches:
-r: stop read from interpreting backslashes (e.g. by default read interprets \w as w)
-d: determines what character ends the read command.
^[ is the character corresponding to Esc, you insert ^[ by pressing Ctrl-V then Esc.
Heredoc and script.
(You said no scripts, but this is quick and dirty, so might as well...)
cat << 'EOF' > scriptonite
print $1 . "\n" if /'(\w+)'/
EOF
then you simply
perl -n scriptonite text

How can I have a newline in a string in sh?

This
STR="Hello\nWorld"
echo $STR
produces as output
Hello\nWorld
instead of
Hello
World
What should I do to have a newline in a string?
Note: This question is not about echo.
I'm aware of echo -e, but I'm looking for a solution that allows passing a string (which includes a newline) as an argument to other commands that do not have a similar option to interpret \n's as newlines.
If you're using Bash, you can use backslash-escapes inside of a specially-quoted $'string'. For example, adding \n:
STR=$'Hello\nWorld'
echo "$STR" # quotes are required here!
Prints:
Hello
World
If you're using pretty much any other shell, just insert the newline as-is in the string:
STR='Hello
World'
Bash recognizes a number of other backslash escape sequences in the $'' string. Here is an excerpt from the Bash manual page:
Words of the form $'string' are treated specially. The word expands to
string, with backslash-escaped characters replaced as specified by the
ANSI C standard. Backslash escape sequences, if present, are decoded
as follows:
\a alert (bell)
\b backspace
\e
\E an escape character
\f form feed
\n new line
\r carriage return
\t horizontal tab
\v vertical tab
\\ backslash
\' single quote
\" double quote
\nnn the eight-bit character whose value is the octal value
nnn (one to three digits)
\xHH the eight-bit character whose value is the hexadecimal
value HH (one or two hex digits)
\cx a control-x character
The expanded result is single-quoted, as if the dollar sign had not
been present.
A double-quoted string preceded by a dollar sign ($"string") will cause
the string to be translated according to the current locale. If the
current locale is C or POSIX, the dollar sign is ignored. If the
string is translated and replaced, the replacement is double-quoted.
Echo is so nineties and so fraught with perils that its use should result in core dumps no less than 4GB. Seriously, echo's problems were the reason why the Unix Standardization process finally invented the printf utility, doing away with all the problems.
So to get a newline in a string, there are two ways:
# 1) Literal newline in an assignment.
FOO="hello
world"
# 2) Command substitution.
BAR=$(printf "hello\nworld\n") # Alternative; note: final newline is deleted
printf '<%s>\n' "$FOO"
printf '<%s>\n' "$BAR"
There! No SYSV vs BSD echo madness, everything gets neatly printed and fully portable support for C escape sequences. Everybody please use printf now for all your output needs and never look back.
What I did based on the other answers was
NEWLINE=$'\n'
my_var="__between eggs and bacon__"
echo "spam${NEWLINE}eggs${my_var}bacon${NEWLINE}knight"
# which outputs:
spam
eggs__between eggs and bacon__bacon
knight
I find the -e flag elegant and straight forward
bash$ STR="Hello\nWorld"
bash$ echo -e $STR
Hello
World
If the string is the output of another command, I just use quotes
indexes_diff=$(git diff index.yaml)
echo "$indexes_diff"
The problem isn't with the shell. The problem is actually with the echo command itself, and the lack of double quotes around the variable interpolation. You can try using echo -e but that isn't supported on all platforms, and one of the reasons printf is now recommended for portability.
You can also try and insert the newline directly into your shell script (if a script is what you're writing) so it looks like...
#!/bin/sh
echo "Hello
World"
#EOF
or equivalently
#!/bin/sh
string="Hello
World"
echo "$string" # note double quotes!
The only simple alternative is to actually type a new line in the variable:
$ STR='new
line'
$ printf '%s' "$STR"
new
line
Yes, that means writing Enter where needed in the code.
There are several equivalents to a new line character.
\n ### A common way to represent a new line character.
\012 ### Octal value of a new line character.
\x0A ### Hexadecimal value of a new line character.
But all those require "an interpretation" by some tool (POSIX printf):
echo -e "new\nline" ### on POSIX echo, `-e` is not required.
printf 'new\nline' ### Understood by POSIX printf.
printf 'new\012line' ### Valid in POSIX printf.
printf 'new\x0Aline'
printf '%b' 'new\0012line' ### Valid in POSIX printf.
And therefore, the tool is required to build a string with a new-line:
$ STR="$(printf 'new\nline')"
$ printf '%s' "$STR"
new
line
In some shells, the sequence $' is a special shell expansion.
Known to work in ksh93, bash and zsh:
$ STR=$'new\nline'
Of course, more complex solutions are also possible:
$ echo '6e65770a6c696e650a' | xxd -p -r
new
line
Or
$ echo "new line" | sed 's/ \+/\n/g'
new
line
A $ right before single quotation marks '...\n...' as follows, however double quotation marks doesn't work.
$ echo $'Hello\nWorld'
Hello
World
$ echo $"Hello\nWorld"
Hello\nWorld
Disclaimer: I first wrote this and then stumbled upon this question. I thought this solution wasn't yet posted, and saw that tlwhitec did post a similar answer. Still I'm posting this because I hope it's a useful and thorough explanation.
Short answer:
This seems quite a portable solution, as it works on quite some shells (see comment).
This way you can get a real newline into a variable.
The benefit of this solution is that you don't have to use newlines in your source code, so you can indent
your code any way you want, and the solution still works. This makes it robust. It's also portable.
# Robust way to put a real newline in a variable (bash, dash, ksh, zsh; indentation-resistant).
nl="$(printf '\nq')"
nl=${nl%q}
Longer answer:
Explanation of the above solution:
The newline would normally be lost due to command substitution, but to prevent that, we add a 'q' and remove it afterwards. (The reason for the double quotes is explained further below.)
We can prove that the variable contains an actual newline character (0x0A):
printf '%s' "$nl" | hexdump -C
00000000 0a |.|
00000001
(Note that the '%s' was needed, otherwise printf will translate a literal '\n' string into an actual 0x0A character, meaning we would prove nothing.)
Of course, instead of the solution proposed in this answer, one could use this as well (but...):
nl='
'
... but that's less robust and can be easily damaged by accidentally indenting the code, or by forgetting to outdent it afterwards, which makes it inconvenient to use in (indented) functions, whereas the earlier solution is robust.
Now, as for the double quotes:
The reason for the double quotes " surrounding the command substitution as in nl="$(printf '\nq')" is that you can then even prefix the variable assignment with the local keyword or builtin (such as in functions), and it will still work on all shells, whereas otherwise the dash shell would have trouble, in the sense that dash would otherwise lose the 'q' and you'd end up with an empty 'nl' variable (again, due to command substitution).
That issue is better illustrated with another example:
dash_trouble_example() {
e=$(echo hello world) # Not using 'local'.
echo "$e" # Fine. Outputs 'hello world' in all shells.
local e=$(echo hello world) # But now, when using 'local' without double quotes ...:
echo "$e" # ... oops, outputs just 'hello' in dash,
# ... but 'hello world' in bash and zsh.
local f="$(echo hello world)" # Finally, using 'local' and surrounding with double quotes.
echo "$f" # Solved. Outputs 'hello world' in dash, zsh, and bash.
# So back to our newline example, if we want to use 'local', we need
# double quotes to surround the command substitution:
# (If we didn't use double quotes here, then in dash the 'nl' variable
# would be empty.)
local nl="$(printf '\nq')"
nl=${nl%q}
}
Practical example of the above solution:
# Parsing lines in a for loop by setting IFS to a real newline character:
nl="$(printf '\nq')"
nl=${nl%q}
IFS=$nl
for i in $(printf '%b' 'this is line 1\nthis is line 2'); do
echo "i=$i"
done
# Desired output:
# i=this is line 1
# i=this is line 2
# Exercise:
# Try running this example without the IFS=$nl assignment, and predict the outcome.
I'm no bash expert, but this one worked for me:
STR1="Hello"
STR2="World"
NEWSTR=$(cat << EOF
$STR1
$STR2
EOF
)
echo "$NEWSTR"
I found this easier to formatting the texts.
Those picky ones that need just the newline and despise the multiline code that breaks indentation, could do:
IFS="$(printf '\nx')"
IFS="${IFS%x}"
Bash (and likely other shells) gobble all the trailing newlines after command substitution, so you need to end the printf string with a non-newline character and delete it afterwards. This can also easily become a oneliner.
IFS="$(printf '\nx')" IFS="${IFS%x}"
I know this is two actions instead of one, but my indentation and portability OCD is at peace now :) I originally developed this to be able to split newline-only separated output and I ended up using a modification that uses \r as the terminating character. That makes the newline splitting work even for the dos output ending with \r\n.
IFS="$(printf '\n\r')"
On my system (Ubuntu 17.10) your example just works as desired, both when typed from the command line (into sh) and when executed as a sh script:
[bash]§ sh
$ STR="Hello\nWorld"
$ echo $STR
Hello
World
$ exit
[bash]§ echo "STR=\"Hello\nWorld\"
> echo \$STR" > test-str.sh
[bash]§ cat test-str.sh
STR="Hello\nWorld"
echo $STR
[bash]§ sh test-str.sh
Hello
World
I guess this answers your question: it just works. (I have not tried to figure out details such as at what moment exactly the substitution of the newline character for \n happens in sh).
However, i noticed that this same script would behave differently when executed with bash and would print out Hello\nWorld instead:
[bash]§ bash test-str.sh
Hello\nWorld
I've managed to get the desired output with bash as follows:
[bash]§ STR="Hello
> World"
[bash]§ echo "$STR"
Note the double quotes around $STR. This behaves identically if saved and run as a bash script.
The following also gives the desired output:
[bash]§ echo "Hello
> World"
I wasn't really happy with any of the options here. This is what worked for me.
str=$(printf "%s" "first line")
str=$(printf "$str\n%s" "another line")
str=$(printf "$str\n%s" "and another line")
This isn't ideal, but I had written a lot of code and defined strings in a way similar to the method used in the question. The accepted solution required me to refactor a lot of the code so instead, I replaced every \n with "$'\n'" and this worked for me.