1

suppose we have this simple script(selfie.sh):

echo "$0"

how do i tell if it is executed this way sh selfie.sh or not, such as cat selfie.sh | sh ?

sh selfie.sh give this:

selfie.sh

and cat selfie.sh | sh output this:

sh

what I have tried:

  • [ -s "$0" ] can't help if there's a file named 'sh' in pwd
  • $BASH_SOURCE or something alike is not compatible with POSIX shell

This question comes in front of me cause I have wrote a project named shell-utils, and I want the user could install like this way if this the first time (s)he installs:

curl -L https://raw.githubusercontent.com/oxnz/shell-utils/master/tool/install | sh

but if (s)he already has a copy of the software and invoke from his shell like this:

sh shell-utils/tool/install.sh

I need to tell the differences and act differently.

oxnz
  • 835
  • 6
  • 16
  • 1
    can you edit your Q to expand on why you need/want to do this? – shellter Dec 04 '15 at 04:44
  • 1
    Executing code directly from`curl` is a security risk, because you don't have a chance to verify what the remote server actually sends before executing it. – chepner Dec 04 '15 at 14:44

2 Answers2

2

Try the -t conditional expression.

$ cat selfie.sh
[ -t 0 ] && echo "bash is reading a script from somewhere else" || echo "bash is reading a script from stdin"
$ cat selfie.sh | sh
bash is reading a script from stdin
$ sh selfie.sh
bash is reading a script from somewhere else
ziyunfei
  • 81
  • 3
  • Thank u, but `sh selfie.sh < selfie.sh` would fail `[ -t 0 ]` test. – oxnz Dec 04 '15 at 06:15
  • @oxnz As you said, this workaround isn't perfect, but I think it's the only way. – ziyunfei Dec 04 '15 at 10:47
  • 1
    @oxnz, then try `[ -t 0 ] && [ -t 1 ]` - check both `stdin` and `stdout`. – Dummy00001 Dec 04 '15 at 11:06
  • @Dummy00001: That doesn't help, because `stdout` will be connected to a terminal in both cases. – mklement0 Dec 04 '15 at 15:11
  • Sorry, I meant `[ -t 0 ] || [ -t 1 ]`. Or more precisely `{ [ -t 0 ] || [ -t 1 ] ; } || ...`. – Dummy00001 Dec 04 '15 at 15:36
  • @Dummy00001: My point was that you cannot use `stdout` to distinguish the cases, because their behavior is identical in that respect - there's no point in testing `-t 1` at all. Perhaps you're mistakenly thinking that `cat selfie.sh | sh` means that `stdout` is not connected to a terminal; while that is true for `sh` itself, it is _not_ true for `selfie.sh`, when `sh` then executes it - and that's where the `-t` tests execute. – mklement0 Dec 07 '15 at 03:05
1

You can use the following POSIX-compliant shell function.

The only prerequisite is a Unix platform where stdin is represented as file /dev/stdin, which is generally the case nowadays.

You'll get a false positive only in one - very unusual - scenario: if, while sourcing a script, you also provide pipeline input; e.g., echo hi | . selfie.sh

#!/bin/sh

# Indicates via exit code whether the contents of the script at hand
# were provided through a pipe, e.g., `curl .... | sh`.
# Caveat: You'll get a false positive in the following - exotic - corner case:
#         ... | . script # combination of pipeline input and sourcing.
isThisScriptPiped() {
  if [ ! -f "$0" ] || [ -x "$0" ] && POSIXLY_CORRECT=1 file -- "$0" | grep -Fvq 'text'; then
    if POSIXLY_CORRECT=1 file -i /dev/stdin | grep -Fq 'fifo'; then
      return 0
    fi
  fi
  return 1
}

# Sample call
isThisScriptPiped && echo 'PIPED' || echo 'NOT piped'

Here's an annotated version of the same function:

#!/bin/sh

# Note: POSIXLY_CORRECT is set below to make the GNU `file` utility behave
#       in a POSIX-compliant manner so as to report the type of the *target*
#       in the event that the operand is a *symlink*.
#       This *could* happen with a shell executable that is a symlink, and 
#       *definitely* happens with /dev/stdin, which on Linux is a symlink to
#       /proc/self/fd/0.

# Indicates via exit code whether the contents of the script at hand
# were provided through a pipe, e.g., `curl .... | sh`.
# Caveat: You'll get a false positive in the following - exotic - corner case:
#         ... | . script # combination of pipeline input and sourcing.
isThisScriptPiped() {

  # Test 1 of 2: Check if $0 refers to:
  #  either: a nonexisting file (implies that $0 refers to an executable in
  #          the path)
  #  or: an executable file that is not text-based (not a shell script)
  # Both cases imply that $0 refers to a shell executable, which in turn implies
  # that no filename argument (script file path) was passed to the shell.
  # Note that while `file` implementations differ, their output for text-based
  # executables (shell scripts) always contains 'text' (POSIX mandates
  # 'commands text', but neither BSD nor GNU `file` do that).
  if [ ! -f "$0" ] || [ -x "$0" ] && POSIXLY_CORRECT=1 file -- "$0" | grep -Fvq 'text'; then

    # The implication is that the script contents comes from:
    #  - either: stdin - whether through input redirection (sh < script) or
    #            from a pipe (... | sh)
    #  - or: from sourcing (. script)
    # Note that in sh there is no way that I know of that lets you determine
    # reliably whether the script is being sourced. Knowing whether the script
    # is being sourced *or* provided via stdin is as close as you can get.
    # (To check for sourcing in Bash, Ksh, or Zsh, see 
    #  http://stackoverflow.com/a/28776166/45375 )

    # Test 2 of 2:
    #  See if stdin is connected to a pipe, which in combination with test 1
    #  implies that the script contents is being piped, EXCEPT in one scenario:
    #  Caveat: You'll get a false positive in the following - very unusual - 
    #          corner case:
    #            ... | . script # combination of sourcing and pipe input
    #  Note:
    #    - POSIX mandates that when passing a FIFO (named pipe) to `file`
    #      the output contain the string 'fifo', which is true of both BSD
    #      and GNU `file`.
    #    - Option -i is crucial to prevent `file` from trying to
    #      read the *contents* of stdin; with -i, it just reports the basic
    #      file type.
    if POSIXLY_CORRECT=1 file -i /dev/stdin | grep -Fq 'fifo'; then
      return 0
    fi

  fi

  return 1

}

# Sample call
isThisScriptPiped && echo 'PIPED' || echo 'NOT piped'
mklement0
  • 382,024
  • 64
  • 607
  • 775