2

In a bash script I've made I am attempting to print out some information to the user (in response to a particular error) suggesting that they add a switch to their command. I thought $0 always contains the text that the user typed as the command at the terminal, so my bash looked like this: echo "try '$0 -s $@' instead."

I've found that when I call that script from a relative path, such as ./script.sh, $0 is "./script.sh" as I expect. However, if script.sh is in my PATH and I call it with script.sh, $0 is not "script.sh". It ends up being the absolute path to the script file instead.

I could use basename $0 to correct the second case, but that would mess up the first. Is there a way to find out exactly what text the user typed for the command that started the script file?

  • This is sort of the opposite of [this question about getting the absolute path of the current script](https://stackoverflow.com/questions/59895/getting-the-source-directory-of-a-bash-script-from-within): that one is about giving the same answer whatever the user typed, this one is about giving exactly what the user typed, even though it's been processed in various ways. – IMSoP May 10 '18 at 18:17
  • Well, there could be many `script.sh` at different locations so absolute path is needed – Diego Torres Milano May 10 '18 at 19:23
  • I wasn’t expecting the full path, but bash on OSX seems to provide it that way. Probably a difference across *NIX variants, or (more likely in my case) bash variants across OSX releases. – Timir May 10 '18 at 19:36
  • 1
    My goal is to echo to the user exactly the command they typed, just with one additional switch. In order to do that, I need to know what command they used to call the script. If it can't be done or if it would be complicated, I'll just not implement it that way, but I'd like to know what I'm doing wrong otherwise. – portalguy15837 May 10 '18 at 20:03
  • 1
    Which platform are you on? On Linux you have `/proc/$$/cmdline`. See https://stackoverflow.com/questions/821837/how-to-get-the-command-line-args-passed-to-a-running-process-on-unix-linux-syste?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – cdarke May 10 '18 at 20:36
  • 1
    By the way, `man bash` for `$0` gives: *If bash is invoked with a file of commands, $0 is set to the name of that file. If bash is started with the -c option, then $0 is set to the first argument after the string to be executed, if one is present. Otherwise, it is set to the filename used to invoke bash, as given by argument zero.* – cdarke May 10 '18 at 20:38
  • `basename ./test.sh test.sh` Why do you think it's messed? – LMC May 10 '18 at 20:41
  • 2
    `$BASH_SOURCE` is the more reliable way to find out where the shell thinks it's getting its source code from, *if* that's even a file on disk at all. That said, "more reliable" is not "reliable". If someone runs `cat file | bash -s`, the shell has no possible way of knowing what the name of `file` is. See [BashFAQ #28](https://mywiki.wooledge.org/BashFAQ/028) for an in-depth discussion. – Charles Duffy May 10 '18 at 21:09

2 Answers2

4

No, you can not generally find out what the user typed.

The user could have:

  • Typed a function or wrapper instead, which runs your and other commands, and which may not pass on parameters.
  • Typed something in a highly non-Bourne shell, like Shelly's run_ "./script" ["--foo"].
  • Clicked a desktop menu instead of typing something.
  • Executed the script from a non-repeatable stream in a Java snippet running on an Android TV.

Canonical behavior is for the caller to set argv[0] to "a filename string that is associated with the process being started" (POSIX), whatever it takes that to mean, and for you to echo that back to them in sh-like usage descriptions.

Unfortunately for scripts, the caller's argv[0] gets lost in the indirection to the shebang interpreter, which instead ends up setting $0 to the filename argument it's interpreting.

It's still fine though. The important thing is for you to identify yourself with basename "$0" or "$0" and indicate why the parameters are wrong. It's the user's responsibility to incorporate the changes into their particular workflow.

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • This answer addresses the crux of my question and provides examples of why the sort of behavior I'm looking for is not easily supportable or necessarily desirable. Thanks! – portalguy15837 May 11 '18 at 22:06
1

I think that the answer is NO.

If I've understood correctly, what you would like to do is create a user_entry variable, so you can write:

echo "try '$user_entry -s $@' instead"

I'll talk about why I think you can't below, but first, let's try to understand the behavior. Very briefly, $0 is the command given to the sub-shell which executes the script.

The reason that what you describe as happening is happening is that executing a script runs the script in a sub-shell. In this sub-shell, the arguments that become $0, $1, $2 are resolved and finalized. To see this, try the following. Here is a program I wrote that will show the concepts that I hope will explain things. (It will also take care of the "additional flag" stuff you describe.)

Go to a directory that is NOT in your PATH, and save the following program. I called it try_it.sh

#/bin/bash

echo "Welcome to the program!"

#print out all of the arguments as given in the subshell
for (( i=0;i<=$#;i++ )); do
  echo "Argument ${i} is: ${!i}"
                         # bash's inderection feature, allows $0, $1, $2, ...

done #endof:  for (( i=0;i<=$#;i++ ))

if [[ $1 != "-s" ]]  ; then
  echo "try '$0 -s $@' instead"
fi #endof: if [[ $1 != "-s" ]]

echo "Working directory: $(pwd)"
echo '$'"0 = $0"
echo "Everything before the script name and last / : "\
'${'"0"'%/*}'" = "${0%/*}
echo "Just the script name: "'${'"0"'##*/}'" = "${0##*/}

while true; do
  echo "Hit Ctrl+C to stop this program's execution."
  sleep 10000 # infinite loop, with instructions on how to get out
done #endof:  while true

Run chmod +x try_it.sh to make sure it's executable.

Now, if possible (it will make things a lot easier to see), close all the shells (terminals).

Open a new terminal (we'll call it Terminal1) and type ps. I hope it makes sense to write the following as another way of describing the exact same action:

Terminal1> ps

This ps tells you what processes are running on the Linux kernel. You should see one bash and one ps in the output. The 1 bash in the output tells you that 1 shells are open, which I hope you can see from your screen.

Open a second terminal, we'll call it Terminal2.

Go back to Terminal1 and type

Terminal1> ps

You should now see two bashs and one ps in the output. (There could be other things, but there are no more than two bashs and no less.) The 2 bashs tell you that 2 shells are open, which I hope you can see from your screen.

Okay. Let's figure out the process of $0. I'm not going to put all of the output here. Hopefully, in seeing the output on your screen (as you try running the script as described,) you can figure out how to create your user_entry variable. I don't actually think you can do this, but I'll try to let you know what's going on.

Let's go to Terminal2. Change directories to the location of your try_it.sh script.

Terminal2> cd /path/to/script/

Run the script (in Terminal2) as

Terminal2> ./try_it.sh 1 2 3

Here's what I saw:

$ ./try_it.sh 1 2 3
Welcome to the program!
Argument 0 is: ./try_it.sh
Argument 1 is: 1
Argument 2 is: 2
Argument 3 is: 3
try './try_it.sh -s 1 2 3' instead
Working directory: /home/me/other_dir
$0 = ./try_it.sh
Everything before the script name and last / : ${0%/*} = .
Just the script name: ${0##*/} = try_it.sh
Hit Ctrl+C to stop this program's execution.

(Don't push "Ctrl+C" just yet.) Note that my /path/to/script/ is /home/me/other_dir/

Return to Terminal1 and run ps again.

Terminal1> ps

You should see 3 bashs. You have your 2 open shells and one sub-shell. Note that the infinite loop is allowing you to "see" the sub-shell. Also, the sleep that you see in the output of ps keeps the "Hit Ctrl+C to stop this program's execution." from repeating on the screen.

Here, we can see that the command "given" to the sub-shell was ./try_it.sh. $0 is the command given to the sub-shell.

Okay, Ctrl+C on Terminal2. Go ahead and ps on Terminal1.

Terminal1> ps

Only 2 bashs. The sub-shell has been closed.

Now, try the following:

Terminal2> /path/to/script/try_it.sh 1 2 3

Terminal1> ps

Number of bashs: 3; command given to sub-shell: /path/to/script/try_it.sh

Try this:

Terminal2> try_it.sh 1 2 3

You get an error. When the path to the executable has not been added to PATH, there needs to be a relative or absolute path to the script for it to execute.

Let's put our script in the PATH. For me (on Cygwin), this is the next command. Make sure you know the correct way to add to PATH on your system. Don't just use this one without checking!

export PATH="$(pwd):$PATH"

Don't worry, this will be gone from your path the next time you close Terminal2.

Now, while you are still in /path/to/script, run

Terminal2> try_it.sh

Terminal1> ps

Number of bashs: 3; command given to sub-shell: /path/to/script/try_it.sh

The "magic of Linux" has gone through the directories in PATH, looking for one that had an executable try_it.sh. The Linux magic connects the correct directory and the script name, then hands that full command to the sub-shell.

Let's make another directory in /path/to/script/

Terminal2> mkdir another_directory
Terminal2> cd another_directory

Let's run it with a relative path.

Terminal2> ../try_it.sh 1 2 3

Terminal1> ps

Number of bashs: 3; command given to sub-shell: ../try_it.sh

A crazier relative path. My /path/to/script/ was really /home/me/other_dir/, so I'll do a crazy relative path as follows, noting that I'm in the another_directory directory.

Terminal2> ../../other_dir/another_directory/../try_it.sh 1 2 3

Terminal1> ps

Number of bashs: 3; command given to sub-shell: ../../other_dir/another_directory/../try_it.sh

We've put the script's path into PATH, so let's try the following from another_directory

Terminal2> try_it.sh

Terminal1> ps

Number of bashs: 3; command given to sub-shell: /path/to/script/try_it.sh

Now, an absolute path. This is run from another_directory

Terminal2> /path/to/script/try_it.sh 1 2 3

Terminal1> ps

Number of bashs: 3; command given to sub-shell: /path/to/script/try_it.sh

Once again, $0 is the command given to the sub-shell.


My thoughts as to the answer to your question

You asked,

Is there a way to find out exactly what text the user typed for the command that started the script file?

I think that the answer is no. If you go through the steps I've shown, I hope you'll understand a little bit of why you got the behavior you did. I think that, in most cases, you can find out what the user typed. However, I see no way to differentiate between these two commands (once /path/to/script has been added to your PATH)

Terminal2> try_it.sh 1 2 3

and

Terminal2> /path/to/script/try_it.sh 1 2 3

Maybe you can figure out a way to tell the difference. If so, post it as the answer.


Note: See this SO post and this link (search "Substring Removal") to get explanations about the ${#%/*} and ${##*/}. Thanks to those people for making it so I don't have to write the explanation.


Just for fun

Let's see if I actually programmed in the correct behavior for when someone does use the -s flag.

$ /path/to/script/try_it.sh -s 1 2 3
Welcome to the program!
Argument 0 is: /home/dblack/other_dir/try_it.sh
Argument 1 is: -s
Argument 2 is: 1
Argument 3 is: 2
Argument 4 is: 3
Working directory: /home/dblack/other_dir
$0 = /home/dblack/other_dir/try_it.sh
Everything before the script name and last / : ${0%/*} = /home/dblack/other_dir
Just the script name: ${0##*/} = try_it.sh
Hit Ctrl+C to stop this program's execution.

It worked. Hooray!

bballdave025
  • 1,347
  • 1
  • 15
  • 28
  • And besides what I've mentioned here, we have the other possibilities pointed out by @that_other_guy in his answer. There's also a very simple case given by @Charles_Duffy in his comment: `cat file | bash -s` – bballdave025 May 10 '18 at 22:26
  • 1
    How about `./try_it.sh "hello world" "$(date +%F)" *.sh` ? In this case it'll say `try './try_it.sh -s hello world 2018-05-10 try_it.sh' instead` , even though none of those arguments were actually present in what the user typed – that other guy May 10 '18 at 23:53
  • 1
    Wow, this is very detailed! Thanks for being so thorough in this description of the mechanism behind calling scripts from a bash shell. – portalguy15837 May 11 '18 at 22:03
  • @portalguy15837, You're welcome! I wouldn't put this as the accepted answer, because it's a bit too much, but it sounded like you wanted to know what was going on. – bballdave025 May 11 '18 at 22:23
  • @thatotherguy. Nice! In addition to providing the answer that I accepted, you came up with some awesome corner cases. At the moment, I have no idea how to get those arguments printed out. Now, it seems, we've found out that not only getting the script name (as entered) is a problem, but so is getting the arguments (as entered.) Great angle! It gives more insight into how the script is handed to the subshell for execution. Excellent point! – bballdave025 May 11 '18 at 22:31