Pass shell parameters as stdin to invoked program - bash

I'm trying to run a program with each of my script's arguments passed as a different line on stdin. My current attempt involves using a for loop, as follows:
#!/bin/bash
[My Program] <<EOF
for i in "$#"
do
$i
done
EOF
This doesn't work -- the program actually receives for i in as part of its input, instead of being given the list of arguments themselves. How would I change it to function?

To feed your program's stdin a newline-separated list of the command-line arguments with which your script was called:
#!/usr/bin/env bash
./your-program <<<"$(printf '%s\n' "$#")"
...or, with POSIX sh-compatible heredoc syntax:
#!/bin/sh
./your-program <<EOF
$(printf '%s\n' "$#")
EOF
If for some reason you really want to use a for loop, you can do that:
#!/bin/sh
./your-program <<EOF
$(for i; do
echo "$i"
done)
EOF
...though note that printf would be a preferable replacement to echo even here; to understand why, see the APPLICATION USAGE section of the POSIX spec for echo.

Related

How can I log the command used to start a bash script? [duplicate]

I know about $*, $#, "$#" and even ${1+"#"} and what they mean.
I need to get access to the EXACT command-line arguments string from a shell script. Please pay attention to the quotes in the example. Anything like "$#" saves parameters but removes quotes and I don't see how to recover from this.
Example:
./my-shell.sh "1 2" 3
And I need to retrieve the EXACT parameter string without any processing:
"1 2" 3
Any idea how to achieve this?
In bash, you can get this from the shell history:
set -o history
shopt -s expand_aliases
function myhack {
line=$(history 1)
line=${line#*[0-9] }
echo "You wrote: $line"
}
alias myhack='myhack #'
Which works as you describe:
$ myhack --args="stuff" * {1..10} $PATH
You wrote: myhack --args="stuff" * {1..10} $PATH
Also, here's a handy diagram:
But I need to exactly reproduce a user-entered command line to be able to programmatically re-execute it in case of failure
You don't need your exact command line for that; you can reconstruct an equivalent (even if not identical!) shell command yourself.
#!/usr/bin/env bash
printf -v cmd_q '%q ' "$#"
echo "Executed with a command identical to: $cmd_q"
...though you don't even need that, because "$#" can be re-executed as-is, without knowing what the command that started it was:
#!/usr/bin/env bash
printf "Program is starting; received command line equivalent to: "
printf '%q ' "$#"
printf '\n'
if [[ ! $already_restarted ]]; then
echo "About to restart ourselves:
exec "$#" # restart the program
fi

Write a user supplied command to file

I need to write a user supplied command to a file with bash.
Testcase would be script.sh -- echo "Hello \$USER" and I want a file containing echo "Hello $USER" (or echo Hello\ $USER) which can be executed
I'm using heredoc to write to the file, so something like
cat > "${FILE}" <<EOF
Your command is:
${CMD}
"$#"
EOF
I tried using printf -v CMD '%q ' "$#" but that leads to echo Hello\ \$USER which is not what I want.
How do I do this?
Based on the suggestion to do it manually I came up with the following using this reasoning:
"$#" syntax means: Expand all args to "$1" "$2" ...
This seems not to work in my case, so do it manually:
CMD=()
for var in "$#"; do
CMD+=( '"'"${var}"'"' )
done
cat > ${FILE} <<EOF
Your command is:
${CMD[#]}
So basically: Create an array of quoted strings. Then print this. This works for the testcase.
But: It fails for plenty of other things. Example: myScript.sh -- echo "Hello \"\$USER\"" which leaves the quotes unescaped.
So I conclude that this task is not possible or at least not feasible and would suggest to use the printf based solution which seems to be also how other commands handle it (example: srun/sbatch on SLURM)

Getting "jobs -l" to show actual command, without unexpanded variables

I executed a background process that was obtained as a parameter and didn't success to get the process's name after the execution.
I do the following:
#! /bin/bash
filePath=$1
$filePath > log.txt &
echo `jobs -l`
Actual result:
[1]+ 2381 Running $filePath > log.txt &
Expected result:
[1]+ 2381 Running /home/user/Desktop/script.sh > log.txt &
The best answer is don't; job control is a feature designed for interactive use, and is not guaranteed to be available at all in noninteractive shells, much less guaranteed to behave in any useful or meaningful way. However, if you insist, you can use printf %q to generate an eval-safe string with the post-expansion form of your variables, and then use eval to run it as code:
#!/bin/bash
printf -v logfile_q '%q' "${log:-log.txt}" # use "$logfile", or default to log.txt
printf -v cmd_q '%q ' "$#" # quote our arguments into one eval-safe string
eval "$cmd_str >$logfile_q &" # Parts that aren't hardcoded must be printf-q'd for safety.
jobs -l
Note that I added some extra configurability for the sake of demonstration -- it's okay to have >log.txt inside your eval'd code, but it's not safe to have >$logfile, because if logfile=$'foo$(rm -rf ~)\'$(rm -rf ~)\'' (a perfectly legal filename!) then you're going to lose your home directory. Thus, any variables needing to be used inside an argument to eval need to be escaped with printf %q beforehand.

Shell POSIX two nested while read and read from stdin not working

I have that sample script:
#!/bin/sh
while read ll </dev/fd/4; do
echo "1 "$ll
while read line; do
echo $line
read input </dev/fd/3
echo "$input"
done 3<&0 <notify-finished
done 4<output_file
Currently The first loop do not iterate just stays on line 1. How do I fix that without bashisms because it has to be highly portable. Thanks.
Your code already has bashisms. Here, I'm taking them out (and simplifying the FD handling for better readability):
#!/bin/sh
while read ll <&4; do # read from output_file
printf '%s\n' "1 $ll"
while read line <&3; do # read from notify-finished
printf '%s\n' "$line"
read input # read from stdin
printf '%s\n' "$input"
done 3<notify-finished
done 4<output_file
Run the script as follows:
echo "output_file" >output_file
echo "notify-finished" >notify-finished
echo "stdout" | ./yourscript
...and it correctly exits with the following output:
1 output_file
notify-finished
stdout
Notes:
echo's behavior is wildly nonportable across POSIX platforms. See the APPLICATION USAGE section of the POSIX spec for echo, which advises using printf instead.
/dev/fd/## is not specified by POSIX; it is an extension made available both by Linux distributions (creating a symlink to /proc/self/fd -- /proc being itself an unspecified extension) and by bash itself. Use <&4 in place of </dev/fd/4.
You probably want to use the -r argument to read -- which is POSIX-specified, and prevents the default behavior of treating backslashes as escape sequences for newlines and characters in IFS. Without it, foo\bar is read as foobar, thus not reading your data as it truly exists in its input sources.

Capturing verbatim command line (including quotes!) to call inside script

I'm trying to write a "phone home" script, which will log the exact command line (including any single or double quotes used) into a MySQL database. As a backend, I have a cgi script which wraps the database. The scripts themselves call curl on the cgi script and include as parameters various arguments, including the verbatim command line.
Obviously I have quite a variety of quote escaping to do here and I'm already stuck at the bash stage. At the moment, I can't even get bash to print verbatim the arguments provided:
Desired output:
$ ./caller.sh -f -hello -q "blah"
-f hello -q "blah"
Using echo:
caller.sh:
echo "$#"
gives:
$ ./caller.sh -f -hello -q "blah"
-f hello -q blah
(I also tried echo $# and echo $*)
Using printf %q:
caller.sh:
printf %q $#
printf "\n"
gives:
$ ./caller.sh -f hello -q "blah"
-fhello-qblah
(I also tried print %q "$#")
I would welcome not only help to fix my bash problem, but any more general advice on implementing this "phone home" in a tidier way!
There is no possible way you can write caller.sh to distinguish between these two commands invoked on the shell:
./caller.sh -f -hello -q "blah"
./caller.sh -f -hello -q blah
There are exactly equivalent.
If you want to make sure the command receives special characters, surround the argument with single quotes:
./caller.sh -f -hello -q '"blah"'
Or if you want to pass just one argument to caller.sh:
./caller.sh '-f -hello -q "blah"'
You can get this info from the shell history:
function myhack {
line=$(history 1)
line=${line#* }
echo "You wrote: $line"
}
alias myhack='myhack #'
Which works as you describe:
$ myhack --args="stuff" * {1..10} $PATH
You wrote: myhack --args="stuff" * {1..10} $PATH
However, quoting is just the user's way of telling the shell how to construct the program's argument array. Asking to log how the user quotes their arguments is like asking to log how hard the user punched the keys and what they were wearing at the time.
To log a shell command line which unambiguously captures all of the arguments provided, you don't need any interactive shell hacks:
#!/bin/bash
line=$(printf "%q " "$#")
echo "What you wrote would have been indistinguishable from: $line"
I understand you want to capture the arguments given by the caller.
Firstly, quotes used by the caller are used to protect during the interpretation of the call. But they do not exist as argument.
An example: If someone call your script with one argument "Hello World!" with two spaces between Hello and World. Then you have to protect ALWAYS $1 in your script to not loose this information.
If you want to log all arguments correctly escaped (in the case where they contains, for example, consecutive spaces...) you HAVE to use "$#" with double quotes. "$#" is equivalent to "$1" "$2" "$3" "$4" etc.
So, to log arguments, I suggest the following at the start of the caller:
i=0
for arg in "$#"; do
echo "arg$i=$arg"
let ++i
done
## Example of calls to the previous script
#caller.sh '1' "2" 3 "4 4" "5 5"
#arg1=1
#arg2=2
#arg3=3
#arg4=4 4
#arg5=5 5
#Flimm is correct, there is no way to distinguish between arguments "foo" and foo, simply because the quotes are removed by the shell before the program receives them. What you need is "$#" (with the quotes).

Resources