Shell script with sudo sh - bash

I want to make a script to install a program (ROS) and I need to write this line:
sudo sh -c 'echo "TEXT VARIABLE TEXT" > systemFile' # to write in systemaFile I need sudo sh
if echo is just fixed text, it works.
If echo is text + variable it doesn't work.
I've tried with:
read f1 < <(lsb_release -a | grep Code* | cut -f2) #codename is writted in variable $f1
echo $f1 # retruns "quantal" as I expected
sudo sh -c 'echo "TEXT $f1 TEXT" > systemFile' #f1 is empty, WHY?
Then I have to assign the variable inside the same instruction sudo sh, for example:
sudo sh -c ' read f1 < <(lsb_release -a | grep Code* | cut -f2) ; echo "TEXT $f1 TEXT" > systemFile'
sh: 1: Syntax error: redirection unexpected

Please try just like this script line
sudo sh -c 'echo TEXT '$f1' TEXT > systemFile'
sudo bash -c 'echo TEXT '$f1' TEXT > systemFile'
i have use this able script line in .sh file and its working fine.

This can work too:
sudo sh -c "echo 'TEXT $VARIABLE TEXT' > systemFile"
However, it is generally not recommended to un-necessarily run a command as sudo. You seem to want only redirection to be "sudoed". So try these options:
echo "TEXT $VARIABLE TEXT" | sudo tee systemFile >/dev/null
echo "TEXT $VARIABLE TEXT" | sudo dd of=systemFile
echo can be simple echo, or any other command you want. Note that this command is not being run under sudo.

use -E option to run sudo:
sudo -E sh -c 'echo "TEXT VARIABLE TEXT" > systemFile'
from man sudo:
-E
The -E (preserve environment) option indicates to the security policy that the user wishes to preserve their existing environment variables. The security policy may return an error if the -E option is specified and the user does not have permission to preserve the environment.

Shell variables are only expanded in "double quotes", not in 'single quotes'.
$ v=value
$ echo $v
value
$ echo "$v"
value
$ echo '$v'
$v
You're starting a new instance of sh which then runs the command echo "TEXT $f1 TEXT" > systemFile.
Since $f1 has not been assigned within the new process, it's empty.
To fix this, you can expand $f1 and pass it in the command line:
sudo sh -c 'echo "TEXT '$f1' TEXT" > systemFile'
Or export it so it's available to the child process (using -E to preserve the environment, thanks anishsane):
export f1
sudo -E sh -c 'echo "TEXT $f1 TEXT" > systemFile'

Related

call a bash function as root but gives 'unexpected end of file'

Why does the last example throw an error but the others work? Bash is invoked in any case.
#!/bin/bash
function hello {
echo "Hello! user=$USER, uid=$UID, home=$HOME";
}
# Test that it works.
hello
# ok
bash -c "$(declare -f hello); hello"
# ok
sudo su $USER bash -c "$(declare -f hello); hello"
# error: bash: -c: line 1: syntax error: unexpected end of file
sudo -i -u $USER bash -c "$(declare -f hello); hello"
It fail because of the -i or --login switch:
It seems like when debugging with -x
$ set -x
$ sudo -i -u $USER bash -c "$(declare -f hello); hello"
++ declare -f hello
+ sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now if doing it manually it cause the same error:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now lets just do a simple tiny change that make it work:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME";}; hello'
The reason is that sudo -i runs everything like an interactive shell. And when doing so, every newline character from the declare -f hello is internally turned into space. The curly-brace code block need a semi-colon before the closing curly-brace when on the same line, which declare -f funcname does not provide since it expands the function source with closing curly brace at a new line.
Now lets make this behaviour very straightforward:
$ sudo bash -c 'echo hello
echo world'
hello
world
It executes both echo statements because they are separated by a newline.
but:
$ sudo -i bash -c 'echo hello
echo world'
hello echo world
It executes the first echo statement that prints everything as arguments because the newline has been replaced by a space.
It is the same code in all examples, so it should be ok.
Yes, "$(declare -f hello); hello" is always the same string. But it is processed differently by sudo su and sudo -i as found out by Lea Gris .
sudo -i quotes its arguments before passing them to bash. This quoting process seems to be undocumented and very poor. To see what was actually executed, you can print the argument passed to bash -c inside your ~/.bash_profile/:
Content of ~/.bash_profile
cat <<EOF
# is executed as
$BASH_EXECUTION_STRING
# resulting in output
EOF
Some examples of sudo -i's terrible and inconsistent quoting
Linebreaks are replaced by line continuations
$ sudo -u $USER -i echo '1
2'
# is executed as
echo 1\
2
# resulting in output
12
Quotes are escaped as literals
$ sudo -u $USER -i echo \'single\' \"double\"
# is executed as
echo \'single\' \"double\"
# resulting in output
'single' "double"
But $ is not quoted
$ sudo -u $USER -i echo \$var
# is executed as
echo $var
# resulting in output
Some side notes:
There might be a misunderstanding in your usage of su.
sudo su $USER bash -c "some command"
does not execute bash -c "echo 1; echo 2". The -c ... is interpreted by su and passed as -c ... to $USER's default shell. Afterwards, the remaining arguments are passed to that shell as well. The executed command is
defaultShellOfUSER -c "some command" bash
You probably wanted to write
sudo su -s bash -c "some command" "$USER"
Interactive shells behave differently
su just executes the command specified by -c. But sudo -i starts a login shell, in your case that login shell seems to be bash (this is not necessarily the case, see section above).
An interactive bash session behaves different from bash -c "..." or bash script.sh. An interactive bash sources files like .profile, .bash_profile, and enables history expansion, aliases, and so on. For a full list see the section Interactive Shell Behavior in bash's manual.

invoke xterm and run command with variable

I would like invoke a xterm with two commands where the first command is to echo some header message follow by some other command (for this sample code I use sleep command for simplicity). The exec command with "echo $msg1" isn't print out any message. Please help me to fix it.
#!/bin/csh -f
set msg1 = ""
set msg1 = "$msg1#[INFO] xx"
set msg1 = "$msg1#[INFO] yy"
# not okay
exec /usr/bin/xterm -e sh -c 'echo "$msg1" | tr "#" "\n" ;sleep 5'
# okay
exec /usr/bin/xterm -e sh -c 'echo hello;sleep 5'
exec /usr/bin/xterm -e sh -c 'echo hello#world | tr "#" "\n" ;sleep 5'
Variables don't work inside single quotes ('), only double quotes ("):
% set x = 'asdf'
% echo '$x'
$x
% echo "$x"
asdf
Right now, the sh process inside the xterm will see echo "$msg1", but it doesn't know about the $msg1 variable since that's local to the script, which is a different process.
You can adjust that command to something like:
exec /usr/bin/xterm -e sh -c "echo '$msg1' | tr '#' '\n' ; sleep 5"
But this won't work well if msg1 can contain single quote or has a \ at the end. Quoting is complex, especially since you're dealing with two different shells (your script and the sh inside xterm) each with its own quoting rules, so it's probably better to use an environment variable:
setenv msg1 "$msg1"
And then you can use the same command as you had above, since the environment variables are inherited by the child process.

OSX Command line: echo command before running it? [duplicate]

In a shell script, how do I echo all shell commands called and expand any variable names?
For example, given the following line:
ls $DIRNAME
I would like the script to run the command and display the following
ls /full/path/to/some/dir
The purpose is to save a log of all shell commands called and their arguments. Is there perhaps a better way of generating such a log?
set -x or set -o xtrace expands variables and prints a little + sign before the line.
set -v or set -o verbose does not expand the variables before printing.
Use set +x and set +v to turn off the above settings.
On the first line of the script, one can put #!/bin/sh -x (or -v) to have the same effect as set -x (or -v) later in the script.
The above also works with /bin/sh.
See the bash-hackers' wiki on set attributes, and on debugging.
$ cat shl
#!/bin/bash
DIR=/tmp/so
ls $DIR
$ bash -x shl
+ DIR=/tmp/so
+ ls /tmp/so
$
set -x will give you what you want.
Here is an example shell script to demonstrate:
#!/bin/bash
set -x #echo on
ls $PWD
This expands all variables and prints the full commands before output of the command.
Output:
+ ls /home/user/
file1.txt file2.txt
I use a function to echo and run the command:
#!/bin/bash
# Function to display commands
exe() { echo "\$ $#" ; "$#" ; }
exe echo hello world
Which outputs
$ echo hello world
hello world
For more complicated commands pipes, etc., you can use eval:
#!/bin/bash
# Function to display commands
exe() { echo "\$ ${#/eval/}" ; "$#" ; }
exe eval "echo 'Hello, World!' | cut -d ' ' -f1"
Which outputs
$ echo 'Hello, World!' | cut -d ' ' -f1
Hello
You can also toggle this for select lines in your script by wrapping them in set -x and set +x, for example,
#!/bin/bash
...
if [[ ! -e $OUT_FILE ]];
then
echo "grabbing $URL"
set -x
curl --fail --noproxy $SERV -s -S $URL -o $OUT_FILE
set +x
fi
shuckc's answer for echoing select lines has a few downsides: you end up with the following set +x command being echoed as well, and you lose the ability to test the exit code with $? since it gets overwritten by the set +x.
Another option is to run the command in a subshell:
echo "getting URL..."
( set -x ; curl -s --fail $URL -o $OUTFILE )
if [ $? -eq 0 ] ; then
echo "curl failed"
exit 1
fi
which will give you output like:
getting URL...
+ curl -s --fail http://example.com/missing -o /tmp/example
curl failed
This does incur the overhead of creating a new subshell for the command, though.
According to TLDP's Bash Guide for Beginners: Chapter 2. Writing and debugging scripts:
2.3.1. Debugging on the entire script
$ bash -x script1.sh
...
There is now a full-fledged debugger for Bash, available at SourceForge. These debugging features are available in most modern versions of Bash, starting from 3.x.
2.3.2. Debugging on part(s) of the script
set -x # Activate debugging from here
w
set +x # Stop debugging from here
...
Table 2-1. Overview of set debugging options
Short | Long notation | Result
-------+---------------+--------------------------------------------------------------
set -f | set -o noglob | Disable file name generation using metacharacters (globbing).
set -v | set -o verbose| Prints shell input lines as they are read.
set -x | set -o xtrace | Print command traces before executing command.
...
Alternatively, these modes can be specified in the script itself, by
adding the desired options to the first line shell declaration.
Options can be combined, as is usually the case with UNIX commands:
#!/bin/bash -xv
Another option is to put "-x" at the top of your script instead of on the command line:
$ cat ./server
#!/bin/bash -x
ssh user#server
$ ./server
+ ssh user#server
user#server's password: ^C
$
You can execute a Bash script in debug mode with the -x option.
This will echo all the commands.
bash -x example_script.sh
# Console output
+ cd /home/user
+ mv text.txt mytext.txt
You can also save the -x option in the script. Just specify the -x option in the shebang.
######## example_script.sh ###################
#!/bin/bash -x
cd /home/user
mv text.txt mytext.txt
##############################################
./example_script.sh
# Console output
+ cd /home/user
+ mv text.txt mytext.txt
Type "bash -x" on the command line before the name of the Bash script. For instance, to execute foo.sh, type:
bash -x foo.sh
Combining all the answers I found this to be the best, simplest
#!/bin/bash
# https://stackoverflow.com/a/64644990/8608146
exe(){
set -x
"$#"
{ set +x; } 2>/dev/null
}
# example
exe go generate ./...
{ set +x; } 2>/dev/null from https://stackoverflow.com/a/19226038/8608146
If the exit status of the command is needed, as mentioned here
Use
{ STATUS=$?; set +x; } 2>/dev/null
And use the $STATUS later like exit $STATUS at the end
A slightly more useful one
#!/bin/bash
# https://stackoverflow.com/a/64644990/8608146
_exe(){
[ $1 == on ] && { set -x; return; } 2>/dev/null
[ $1 == off ] && { set +x; return; } 2>/dev/null
echo + "$#"
"$#"
}
exe(){
{ _exe "$#"; } 2>/dev/null
}
# examples
exe on # turn on same as set -x
echo This command prints with +
echo This too prints with +
exe off # same as set +x
echo This does not
# can also be used for individual commands
exe echo what up!
For zsh, echo
setopt VERBOSE
And for debugging,
setopt XTRACE
To allow for compound commands to be echoed, I use eval plus Soth's exe function to echo and run the command. This is useful for piped commands that would otherwise only show none or just the initial part of the piped command.
Without eval:
exe() { echo "\$ $#" ; "$#" ; }
exe ls -F | grep *.txt
Outputs:
$
file.txt
With eval:
exe() { echo "\$ $#" ; "$#" ; }
exe eval 'ls -F | grep *.txt'
Which outputs
$ exe eval 'ls -F | grep *.txt'
file.txt
For csh and tcsh, you can set verbose or set echo (or you can even set both, but it may result in some duplication most of the time).
The verbose option prints pretty much the exact shell expression that you type.
The echo option is more indicative of what will be executed through spawning.
http://www.tcsh.org/tcsh.html/Special_shell_variables.html#verbose
http://www.tcsh.org/tcsh.html/Special_shell_variables.html#echo
Special shell variables
verbose
If set, causes the words of each command to be printed, after history substitution (if any). Set by the -v command line option.
echo
If set, each command with its arguments is echoed just before it is executed. For non-builtin commands all expansions occur before echoing. Builtin commands are echoed before command and filename substitution, because these substitutions are then done selectively. Set by the -x command line option.
$ cat exampleScript.sh
#!/bin/bash
name="karthik";
echo $name;
bash -x exampleScript.sh
Output is as follows:

How to execute arbitrary command under `bash -c`

What is a procedure to decorate an arbitrary bash command to execute it in a subshell? I cannot change the command, I have to decorate it on the outside.
the best I can think of is
>bash -c '<command>'
works on these:
>bash -c 'echo'
>bash -c 'echo foobar'
>bash -c 'echo \"'
but what about the commands such as
echo \'
and especially
echo \'\"
The decoration has to be always the same for all commands. It has to always work.
You say "subshell" - you can get one of those by just putting parentheses around the command:
x=outer
(x=inner; echo "x=$x"; exit)
echo "x=$x"
produces this:
x=inner
x=outer
You could (ab)use heredocs:
bash -c "$(cat <<-EOF
echo \'\"
EOF
)"
This is one way without using -c option:
bash <<EOF
echo \'\"
EOF
What you want to do is exactly the same as escapeshellcmd() in PHP (http://php.net/manual/fr/function.escapeshellcmd.php)
You just need to escape #&;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and " are escaped only if they are not paired.
But beware of security issues...
Let bash take care of it this way:
1) prepare the command as an array:
astrCmd=(echo \'\");
2) export the array as a simple string:
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
3) restore the array and run it as a full command:
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}"
Create a function to make these steps more easy like:
FUNCbash(){
astrCmd=("$#");
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}";
}
FUNCbash echo \'\"

sudo echo "something" >> /etc/privilegedFile doesn't work [duplicate]

This question already has answers here:
How do I use sudo to redirect output to a location I don't have permission to write to? [closed]
(15 answers)
Closed 1 year ago.
This is a pretty simple question, at least it seems like it should be, about sudo permissions in Linux.
There are a lot of times when I just want to append something to /etc/hosts or a similar file but end up not being able to because both > and >> are not allowed, even with root.
Is there someway to make this work without having to su or sudo su into root?
Use tee --append or tee -a.
echo 'deb blah ... blah' | sudo tee -a /etc/apt/sources.list
Make sure to avoid quotes inside quotes.
To avoid printing data back to the console, redirect the output to /dev/null.
echo 'deb blah ... blah' | sudo tee -a /etc/apt/sources.list > /dev/null
Remember about the (-a/--append) flag!
Just tee works like > and will overwrite your file. tee -a works like >> and will write at the end of the file.
The problem is that the shell does output redirection, not sudo or echo, so this is being done as your regular user.
Try the following code snippet:
sudo sh -c "echo 'something' >> /etc/privilegedfile"
The issue is that it's your shell that handles redirection; it's trying to open the file with your permissions not those of the process you're running under sudo.
Use something like this, perhaps:
sudo sh -c "echo 'something' >> /etc/privilegedFile"
sudo sh -c "echo 127.0.0.1 localhost >> /etc/hosts"
Doing
sudo sh -c "echo >> somefile"
should work. The problem is that > and >> are handled by your shell, not by the "sudoed" command, so the permissions are your ones, not the ones of the user you are "sudoing" into.
I would note, for the curious, that you can also quote a heredoc (for large blocks):
sudo bash -c "cat <<EOIPFW >> /etc/ipfw.conf
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<plist version=\"1.0\">
<dict>
<key>Label</key>
<string>com.company.ipfw</string>
<key>Program</key>
<string>/sbin/ipfw</string>
<key>ProgramArguments</key>
<array>
<string>/sbin/ipfw</string>
<string>-q</string>
<string>/etc/ipfw.conf</string>
</array>
<key>RunAtLoad</key>
<true></true>
</dict>
</plist>
EOIPFW"
In bash you can use tee in combination with > /dev/null to keep stdout clean.
echo "# comment" | sudo tee -a /etc/hosts > /dev/null
Some user not know solution when using multiples lines.
sudo tee -a /path/file/to/create_with_text > /dev/null <<EOT
line 1
line 2
line 3
EOT
Using Yoo's answer, put this in your ~/.bashrc:
sudoe() {
[[ "$#" -ne 2 ]] && echo "Usage: sudoe <text> <file>" && return 1
echo "$1" | sudo tee --append "$2" > /dev/null
}
Now you can run sudoe 'deb blah # blah' /etc/apt/sources.list
Edit:
A more complete version which allows you to pipe input in or redirect from a file and includes a -a switch to turn off appending (which is on by default):
sudoe() {
if ([[ "$1" == "-a" ]] || [[ "$1" == "--no-append" ]]); then
shift &>/dev/null || local failed=1
else
local append="--append"
fi
while [[ $failed -ne 1 ]]; do
if [[ -t 0 ]]; then
text="$1"; shift &>/dev/null || break
else
text="$(cat <&0)"
fi
[[ -z "$1" ]] && break
echo "$text" | sudo tee $append "$1" >/dev/null; return $?
done
echo "Usage: $0 [-a|--no-append] [text] <file>"; return 1
}
You can also use sponge from the moreutils package and not need to redirect the output (i.e., no tee noise to hide):
echo 'Add this line' | sudo sponge -a privfile
By using sed -i with $ a , you can append text, containing both variables and special characters, after the last line.
For example, adding $NEW_HOST with $NEW_IP to /etc/hosts:
sudo sed -i "\$ a $NEW_IP\t\t$NEW_HOST.domain.local\t$NEW_HOST" /etc/hosts
sed options explained:
-i for in-place
$ for last line
a for append
echo 'Hello World' | (sudo tee -a /etc/apt/sources.list)
How about:
echo text | sudo dd status=none of=privilegedfile
I want to change /proc/sys/net/ipv4/tcp_rmem.
I did:
sudo dd status=none of=/proc/sys/net/ipv4/tcp_rmem <<<"4096 131072 1024000"
eliminates the echo with a single line document
This worked for me:
original command
echo "export CATALINA_HOME="/opt/tomcat9"" >> /etc/environment
Working command
echo "export CATALINA_HOME="/opt/tomcat9"" |sudo tee /etc/environment
Can you change the ownership of the file then change it back after using cat >> to append?
sudo chown youruser /etc/hosts
sudo cat /downloaded/hostsadditions >> /etc/hosts
sudo chown root /etc/hosts
Something like this work for you?

Resources