17

I cannot figure out what's wrong with this. When I run it in terminal and enter password, nothing happens, but if I run every command separately in terminal, it works. Thank you!

#!/bin/bash    

sudo su;
mkdir /opt/D3GO/;
cp `pwd`/D3GO /opt/D3GO/;
cp `pwd`/D3GO.png /opt/D3GO/;
cp `pwd`/D3GO.desktop /usr/share/applications/;
chmod +x /opt/D3GO/D3GO
mklement0
  • 382,024
  • 64
  • 607
  • 775
Dusan Milosevic
  • 460
  • 2
  • 4
  • 18
  • 1
    As an aside, `"$PWD"` is **much** more efficient to evaluate than `$(pwd)` or its backtick equivalent; every time you run a command substitution you're `fork()`ing off a subprocess, running the given command (in this case `pwd`) in that separate process, and then reading its output over a pipeline; whereas `$PWD` is evaluated in the parent shell directly. – Charles Duffy Apr 07 '17 at 14:29
  • See also https://stackoverflow.com/questions/37586811/pass-commands-as-input-to-another-command-su-ssh-sh-etc – tripleee Jul 19 '17 at 03:34

6 Answers6

21

You can use Here Documents to redirect input into an interactive shell script. The operator << is an instruction to read input until it finds a line containing the specified delimiter, as EOF (end of file).

sudo su <<EOF
echo "code"
EOF

e.g.

#!/bin/bash    
sudo su <<EOF
mkdir /opt/D3GO/
cp `pwd`/D3GO /opt/D3GO/
cp `pwd`/D3GO.png /opt/D3GO/
cp `pwd`/D3GO.desktop /usr/share/applications/
chmod +x /opt/D3GO/D3GO
EOF
acfreitas
  • 1,347
  • 2
  • 13
  • 27
19

Command sudo su starts an interactive root shell, but it will not convert the current shell into a root one.

The idiom to do what you want is something along the lines of this (thanks to @CharlesDuffy for the extra carefulness):

#check for root
UID=$(id -u)
if [ x$UID != x0 ] 
then
    #Beware of how you compose the command
    printf -v cmd_str '%q ' "$0" "$@"
    exec sudo su -c "$cmd_str"
fi

#I am root
mkdir /opt/D3GO/
#and the rest of your commands

The idea is to check whether the current user is root, and if not, re-run the same command with su

rodrigo
  • 94,151
  • 12
  • 143
  • 190
  • 2
    `"$0 $@"` is not right -- breaks horribly with whitespace, glob characters, etc, because it's trying to flatten `$@` (an array) into a string. `printf -v cmd_str '%q ' "$0" "$@"; exec sudo su -c "$cmd_str"` would be closer. – Charles Duffy Jul 08 '14 at 19:41
  • @CharlesDuffy: Well, I think you are right, the arguments are expanded twice. I guess you are never careful enough with root scripts. – rodrigo Jul 08 '14 at 19:44
  • 1
    (While nitpicking, by the way -- the `[ x$foo = xbar ]` practice isn't needed in any POSIX.2-compatible shell -- it's a holdout from the Bourne days -- but if you want some correctness-related paranoia, you should really quote: `[ "$UID" != 0 ]`; otherwise, even with the `x`s you'll get syntax errors if your expanded string splits into multiple words... which can even happen for `$UID` if your `IFS` contains numeric characters). – Charles Duffy Jul 08 '14 at 19:46
  • This works, @rodrigo could you update the answer if Charle's solution is better, because I'm very new with scripts. – Dusan Milosevic Jul 08 '14 at 19:47
  • @CharlesDuffy: I like nitpicking as much as anyone... I copied this command from an embedded system of mine where the `x` is needed. And about the arguments, since they are not needed by the OP, they could be dropped altogether, but the `$0` is still expanded twice (what if your script is named `*`?) – rodrigo Jul 08 '14 at 19:49
  • @DusanMilosevic, ...by the way, since this approach trusts `$0` to be correct, it's at least somewhat potentially error-prone. See http://mywiki.wooledge.org/BashFAQ/028 for an in-depth discussion. – Charles Duffy Jul 08 '14 at 19:49
  • 1
    @rodrigo, are you sure the `x`s were *really* needed? If you weren't quoting (and, after all, you didn't quote here), you'd see errors in the empty-string case... but adding the `x`s *only* fixes the empty-string case, whereas adding quotes fixes both the empty-string case and the multiple-words case. – Charles Duffy Jul 08 '14 at 19:50
  • @rodrigo, ...I say that being familiar with the shells used in modern embedded systems; they're mostly `ash` derivatives, which are in fact compliant with POSIX.2. – Charles Duffy Jul 08 '14 at 19:51
  • @CharlesDuffy: What about `printf -v cmd_str "'%q' " "$0" "$@"`, With the extra `''` in the format string? Wouldn't that be better? – rodrigo Jul 08 '14 at 19:52
  • 1
    @rodrigo, no, it would not. `printf %q` is responsible for emitting a string which can be safely eval'd exactly as-is. – Charles Duffy Jul 08 '14 at 19:53
  • @CharlesDuffy: Oh!, the `%q` takes care of everthing. Sweet. – rodrigo Jul 08 '14 at 19:55
  • I have added `mkdir ~/.D3GO/; cp -a `pwd`/viewright_backup/. ~/.D3GO/` These commands, and it doesn't work. The ./D3GO folder is not created. Do you know why? Thank you so much! – Dusan Milosevic Jul 08 '14 at 20:22
  • @DusanMilosevic: Use `$HOME` instead of `~`. Probably you have the directory created under `/root` instead of your user home. – rodrigo Jul 08 '14 at 20:25
  • Nope, it still creates a folder in root. But thanks, now, when I know that, I know what to do. Thanks! – Dusan Milosevic Jul 08 '14 at 20:30
  • 2
    Nicely done; taking full advantage of `bash` features and replacing `sudo su -c` with `sudo -s`, the solution can be simplified to `[[ $(id -u) -eq 0 ]] || exec sudo -s "$BASH_SOURCE" $(printf '%q ' "$@")` – mklement0 Apr 04 '16 at 02:39
9

sudo su is not a command run within a shell -- it starts a new shell.

That new shell is no longer running your script, and the old shell that is running the script waits for the new one to exit before it continues.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
9

The accepted answer works well, but the idiom for a script re-invoking itself with sudo on demand can be simplified and made more portable:

[[ $(id -u) -eq 0 ]] || exec sudo /bin/bash -c "$(printf '%q ' "$BASH_SOURCE" "$@")"
  • Using [[ ... ]] instead of [ ... ] makes prepending x to the operands (or double-quoting the LHS) unnecessary.

  • Using bash -c instead of su -c to interpret the reconstructed command line makes the command more portable, because not all platforms support su -c (e.g., macOS doesn't).

  • In bash, $BASH_SOURCE is generally the more reliable way to refer to the running script.


With the above approach, any variable references or command / arithmetic substitutions in the arguments are invariably expanded by the calling shell.

If you instead you wanted delayed expansion - so that variable references aren't expanded until the sudo shell runs, in the context of the root user - use this:

(( __reinvoked )) || exec sudo -s __reinvoked=1 "$BASH_SOURCE" "$@"

Note that you'd then have to single-quote any arguments containing variable references or command substitutions for them to be delayed-expanded; e.g., '$USER'.

Note the use of ad-hoc environment variable __reinvoked to ensure re-invocation exactly once (even when initially already invoked as the root user).


Here's a sample script that demonstrates the first technique:

  • If not invoked as root, the script reinvokes itself with sudo -s, passing all arguments through as-is.

  • Unless previously authenticated and still within the timeout period, sudo will prompt for an administrator password.

#!/bin/bash

[[ $(id -u) -eq 0 ]] || exec sudo /bin/bash -c "$(printf '%q ' "$BASH_SOURCE" "$@")"

# Print the username and all arguments.
echo "Running as: $(id -un)"
echo "Arguments:"
for arg; do echo "  $((++i)): [$arg]"; done

acfreitas's helpful answer demonstrates a "script-inside-a-script" technique where a here-document is used to provide shell code via stdin to sudo su.
Again, sudo -s is sufficient and quoting is important:

sudo -s -H <<'EOF'
echo "$HOME"
EOF

Note how the opening here-document delimiter, EOF in this case, is quoted in order to prevent the contents of the document from up-front interpretation by the current shell.
If you didn't quote (any part of) EOF, $HOME would be expand to the current user's home directory.

If you want to mix up-front and delayed expansion, leave the opening here-document delimiter unquoted and selectively \-quote $ instances:

sudo -s -H <<EOF
echo "Called by: $USER; root's home dir: \$HOME"
EOF
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
2

Because running "sudo su" opens a new shell and the command does not return until you exit from that shell. Perhaps split the script into 2 files: first one runs sudo and executes that 2nd script under sudo.

MK.
  • 33,605
  • 18
  • 74
  • 111
2

sudo su will attempt to start a new shell as root. Once that new shell is opened, the original script will not continue until the new shell is closed.

For a fix try:

In the shell script try:

su <username> -c "my command"

So if the user was "userA":

su userA -c "mkdir /opt/D3GO/"

However, if you are userA for example and you want to run the part of script as root, you will be prompted for a pass.

su root -c "mkdir /opt/D3GO/"

You can also get around that by just running the script with sudo in the first place

sudo ./myScript.sh

That way the script retains the original user inside the script which you can access using the standard variables like ${USERNAME}, ${UID} etc

Depends on what works better for you.

Amos
  • 182
  • 10