bourne shell == line noise
March 22, 2007 3:31 PM   Subscribe

Please help me understand what this OS X shell script is doing.

On OS X 10.4, the 'cd' command is a shell script in /usr/bin/cd:

#!/bin/sh
${0##*/} ${1+"$@"}

Can someone explain it to me?
posted by Armitage Shanks to Computers & Internet (19 answers total) 5 users marked this as a favorite
 
cd's a builtin command in the shell. It's calling the builtin sh cd command with the argument. Presumably it's there for use when you're not in a shell environment to cd inside.
posted by edd at 3:38 PM on March 22, 2007


I should clarify that the first argument is the name of the script, so this is the same script that handles fg, alias and other otherwise-builtin commands - you can see that it just repeats what you put in if you copy it elsewhere, put an 'echo' in before it, and run it from the other location.

Also, on poking more knowledgeable types the ##*/ is responsible for eliminating the path if you try to put one in, so /usr/bin/cd gets reduced down to just cd. Not too sure about the nature of the thing that provides the arguments.
posted by edd at 3:47 PM on March 22, 2007


cd
posted by donpardo at 3:48 PM on March 22, 2007


edd, yes,
cd's a builtin command in the shell.
and indeed, it has to be—the script is puzzling, since to my knowledge it can't change the working directory of its parent process without using some application-specific IPC to do so, and as such, what it does is start a new shell, change that shell's current directory to the specified directory, then exit from it, returning to the parent shell, with the original working directory. Useless.
posted by Aidan Kehoe at 3:53 PM on March 22, 2007


I should clarify that the first argument is the name of the script, so this is the same script that handles fg, alias and other otherwise-builtin commands - you can see that it just repeats what you put in if you copy it elsewhere, put an 'echo' in before it, and run it from the other location.
Ah, okay, so it may have been linked to cd because someone thought it sensible to provide shell script versions of all the built-in commands, even where that made no sense.
posted by Aidan Kehoe at 3:55 PM on March 22, 2007


Ah yes, good point, but as donpardo's link points out it can be used to check if you can cd into a directory, even if it doesn't actually get the parent there.

Another knowledgeable type tells me that the noise in the second argument is to help deal with no argument. It stops attempts to do a 'cd ""' if you don't give a second argument in some cases, and just does the 'cd' instead.
posted by edd at 3:59 PM on March 22, 2007


Can't it be used to see if cd'ing to a specific directory is possible (by branching on the return code) before attempting to cd to that directory?
posted by vacapinta at 4:00 PM on March 22, 2007


Best answer: ${0##*/} -> $0 expands to the name of the script, ## removes the largest prefix pattern, and */ tells it to match until the last / in the string: this turns "/usr/bin/cd" into "cd".

${1+"$@"} -> test if $1 (first argument) is unset, if not, expand the argument list ($@) without field spliting (so you don't expand "cd /foo\ bar/" into "cd '/foo' 'bar/'")

man sh :)
posted by Freaky at 4:08 PM on March 22, 2007


It may shed some light on this situation if one knows that /usr/bin/alias, bg, cd, command, fc, fg, getopts, hash, jobs, read, type, ulimit, umask, unalias, and wait ALL look like this on OSX:
#!/bin/sh
# $FreeBSD: src/usr.bin/alias/generic.sh,v 1.1 2002/07/16 22:16:03 wollman Exp $
# This file is in the public domain.
${0##*/} ${1+"$@"}

posted by dmd at 4:51 PM on March 22, 2007


Response by poster: It may shed some light on this situation if one knows that /usr/bin/alias, bg, cd, command, fc, fg, getopts, hash, jobs, read, type, ulimit, umask, unalias, and wait ALL look like this on OSX:

Interesting. It appears that none of these files exist in 10.3.

Here's a checkin I found for the file generic.sh on FreeBSD that may explain it (it confuses me even more, to be honest):
Use the "builtin" shell function to make sure that the requested
command is handled as a shell function. This avoids the following
peculiar behaviour when /usr/bin is on a case-insensitive filesystem:
# READ foo
(... long pause, depending upon the amount of swap space available ...)
sh: Resource temporarily unavailable.
posted by Armitage Shanks at 5:00 PM on March 22, 2007


cmiller@calliope:~ $ /usr/bin/cd /tmp
cmiller@calliope:~ $

Indeed, what a strange little program.
posted by cmiller at 5:03 PM on March 22, 2007


Response by poster: Damn, this gets weirder all the time:
$ which cd
/usr/bin/cd
$ which CD
/usr/bin/CD
$ cd
$ CD
/usr/bin/CD: fork: Resource temporarily unavailable
If I edit /usr/bin/cd and add an "echo FOO" to it, when I execute "CD" (in caps), it goes into an infinite loop printing "FOO" until it runs out of processes. So it seems like it's not even preventing the problem it was meant to. Wtf.
posted by Armitage Shanks at 5:16 PM on March 22, 2007


Response by poster: Actually, it seems to *introduce* the problem. On 10.3, if you try to run any of the builtins in caps, i.e.

CD
ALIAS
UNALIAS
etc

you just get "command not found". On 10.4, they all eventually run out of resources because the shell script version is presumably invoking itself recursively.
posted by Armitage Shanks at 5:23 PM on March 22, 2007


Yes, that's why the builtin function was added to it; builtin lookup is case-sensitive, so if filename lookup isn't, and you don't use the same case as the builtin to call the script, you get stuck in a loop as it tries to run $0. Explicitly telling the shell to use a builtin fixes this:

#!/bin/sh
# $FreeBSD: src/usr.bin/alias/generic.sh,v 1.1.16.1 2005/11/04 18:21:37 cperciva Exp $
# This file is in the public domain.
builtin ${0##*/} ${1+"$@"}


You crazy kids and your case-insensitive filesystems, you'll get us all killed!
posted by Freaky at 7:45 PM on March 22, 2007


I still don't understand why

${1+"$@"}

works. I mean, it clearly does, but all the sh and bash manuals I've ever seen say that the proper syntax for this should be

${1:+"$@"}

Is the first form just a terribly-old-skool version of the second?
posted by flabdablet at 7:53 PM on March 22, 2007


Response by poster: Yes, that's why the builtin function was added to it;

Ah yes, I missed that, scratch my last two comments.

OK, so the only thing I'm not clear on is why /usr/bin equivalents for all these builtins (alias, bg, cd, command, fc, fg, getopts, hash, jobs, read, type, ulimit, umask, and unalias) were added in the first place.
posted by Armitage Shanks at 8:00 PM on March 22, 2007


Best answer: "all the sh and bash manuals I've ever seen say that the proper syntax for this should be

${1:+"$@"}

Is the first form just a terribly-old-skool version of the second?"


Nope; it changes the behavior wrt null arguments, allowing for the expansion of empty strings. From sh(1):

${parameter:+word}
Use Alternate Value. If parameter is unset or null, null is substituted; otherwise, the expansion of word is substituted.

In the parameter expansions shown previously, use of the colon in the format results in a test for a parameter that is unset or null; omission of the colon results in a test for a parameter that is only unset.


You can see this behavior on a simple script like:

#!/bin/sh
ruby -e 'p ARGV' ${1+"$@"}
ruby -e 'p ARGV' ${1:+"$@"}


When ran with ./foo.sh ' bla you get:

["", "bla"]
[]


-- the second doesn't get expanded because with the colon, there's nothing to distinguish an empty (null) argument and no argument (unset).

As for their reason for existance, check the CVS logs: they're required by POSIX. Well, that's why they're in FreeBSD (and by proxy why they're in MacOS X); why they're in POSIX I don't know ;)
posted by Freaky at 11:11 AM on March 23, 2007


Thanks for that, freaky. You'd think I would have learned by now to read the sh manual with a fine tooth comb :-)

Okay. So ${1+"$@"} means "if argument 1 is set to anything at all, substitute all arguments individually quoted".

Then, because this happens inside an unquoted $1 expansion, the resulting value will be word-split. But because "$@" generates individually quoted values, word-splitting will only occur on the boundaries between those quoted values; in effect, the expansion of the substituted $1 will be all arguments, individually quoted.

So why would anybody use this convoluted test-and-substitute construction instead of a bare "$@"? Can you think of an argument list that would make

#!/bin/sh
ruby -e 'p ARGV' ${1+"$@"}
ruby -e 'p ARGV' "$@"

print two different output lines? I haven't been able to find one.
posted by flabdablet at 6:20 PM on March 24, 2007


Pass; my reading of the manpage and tests suggest that indeed they're equivilent. The scripts are pretty much utterly useless anyway, so I guess useless tests within them are to be expected ;)

Maybe it's defensive, in case of a shell which doesn't expand "$@" properly in the zero-arguments case.

Asking on a mailing list may get more detail. *someone* on freebsd-*@freebsd.org probably knows ;)
posted by Freaky at 7:56 PM on March 24, 2007


« Older Apartment Security: How can I make my particular...   |   What's the best e-book reader? Newer »
This thread is closed to new comments.