Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I want to split a string $basename, containing the output in two parts, $stem and $ext, such that:

  1. the string "${stem}${ext}" is identical to the original string $basename;
  2. either $stem or $ext may be empty (depending on the string in $basename);
  3. if $ext is not empty, it must start with ., and contain no more .'s thereafter.

It's not difficult to write a shell function to do this, but before doing so I'd like to know if there's a standard Unix command that does this.

EDIT:

FWIW, the way I'd code this in a (zsh) script would be

stem=${basename%.*}
ext=${basename#"$stem"}

EDIT: fixed typo (${base#... -> ${basename#...), and incorporated Stephane Chazelas' suggestion (...#$stem -> ...#"$stem").

share|improve this question
1  
Are you counting normal string parsing tools like sed and awk? Other than those and similar tricks like bash string manipulations, no, I don't think there's a tool dedicated to splitting a filename that way –  Michael Mrozek Jun 27 '13 at 17:25
1  
Standard Unix Tool? Besides sh or bash or sed or awk or perl? What you're suggesting is a very basic string operation that very nearly every interpreter can do; most of which are "standard" tools. –  tylerl Jun 27 '13 at 20:08
    
@MichaelMrozek, I think kjo is looking for some tool (like an enhanced basename) that does just that and handles all the corner cases without having to implement it in shell/sed/awk/younameit –  Stephane Chazelas Jun 27 '13 at 20:46
add comment

5 Answers

up vote 2 down vote accepted

There can't be commands that set variables of your shell, because variables are something internal to the shell process, so another command, living in its own process, couldn't possibly alter that.

With your shell, you can do things like:

var=$(some-command)

to retrieve the output of a command (without the trailing newline characters), but if you need two outcomes of a command, that's when it becomes trickier.

One method is like:

eval "$(some-command)"

Where some-command outputs things like:

var1='something' var2='someotherthing'

But before you ask, there's no such standard command that takes a path and splits it into the dir, basename and extension (whatever that means) in such a way.

Now the shells themselves may have internal features for that. For instance csh and zsh have modifiers that can give you the head, tail, extension. Like in zsh:

file=/path/to/foo.bar/
head=$file:h
tail=$file:t
ext=$file:e
rest=$file:r

Now you may want to consider what those should be for a file like . or .., /, .bashrc or foo.tar.gz?

Now if you're looking for standard POSIX syntax, then you have it already (almost): rest=${file%.*}. ${file#$rest} is zsh specific. In POSIX syntax, you need ext=${file#"$rest"}, otherwise $rest is taken as a pattern. Beware that may not do what you want if $file contains a path with / characters (like foo.d/bar).

share|improve this answer
    
Thanks for the clarification... I see that I would need two commands (or one command with two different "modes" of operation), and two calls, to do what I want. And also for the pointer about double-quoting the $rest part. –  kjo Jun 28 '13 at 0:41
    
Well said answer! I'd +2 if I could. –  slm Jun 28 '13 at 1:07
add comment

You can use just plain string parsing functions that are included in Bash to do something very close to what you want:

$ F="foo.bar.baz"

$ echo ${F%.*}
foo.bar

$ echo ${F/*./.}
.baz

So you could put it together like so:

$ $ F="foo.bar.baz"

$ stem=${F%.*}
$ ext=${F/*./.}

echo "${stem}${ext}"
foo.bar.baz

excerpts from Bash man page

${F%.*}
${parameter%word}
${parameter%%word}
   Remove  matching suffix pattern.  The word is expanded to produce a pattern 
   just as in pathname expansion.  If the pattern matches a trailing portion
   of the expanded value of parameter, then the result of  the  expansion  is
   the expanded  value  of  parameter with the shortest matching pattern (the
   ``%'' case) or the longest matching pattern (the ``%%'' case) deleted.  If
   parameter is @ or *, the pattern removal operation is applied  to  each
   positional parameter  in turn, and the expansion is the resultant list.  
   If parameter is an array variable subscripted with @ or *, the pattern
   removal operation is applied to each member of the array in  turn,  and  
   the  expansion  is  the resultant list.
${/*./.}
${parameter/pattern/string}
   Pattern  substitution.   The pattern is expanded to produce a pattern just
   as in pathname expansion.  Parameter is expanded and the longest match of 
   pattern against its value is replaced with string.  If pattern  begins 
   with  /, all  matches  of  pattern are replaced with string.  Normally
   only the first match is replaced.  If pattern begins with #, it must match
   at the beginning of the expanded value of parameter.  If pattern  begins
   with  %,  it  must match  at the end of the expanded value of parameter.
   If string is null, matches of pattern are deleted and the / following
   pattern may be omitted.  If parameter is @ or *, the substitution operation
   is  applied  to  each  positional  parameter in turn, and the expansion 
   is the resultant list.  If parameter is an array variable subscripted
   with @ or *, the substitution operation is applied to each member of the
   array in turn, and the expansion  is  the resultant list.

You can read about these features in the Bash man page.

share|improve this answer
    
What was wrong with ${F/*./.}? –  rici Jun 27 '13 at 19:25
    
@rici - that works too. Hadn't tried it. –  slm Jun 27 '13 at 19:40
    
@rici - updates the answer, thanks for the feedback. –  slm Jun 27 '13 at 19:54
    
It's important to note that this solution is POSIX shell-compatible, which means it should work in any shell that supports the IEEE Std 1003.1 Shell Command Language (see pubs.opengroup.org/onlinepubs/9699919799/utilities/…) –  bonsaiviking Jun 27 '13 at 21:10
    
@bonsaiviking, no. ${F/*./.} is not POSIX. –  Stephane Chazelas Jun 27 '13 at 21:31
show 2 more comments

If the strings do not contain newlines:

start cmd:> echo foo.bar.baz | sed 's/\(^.*\)\(\.[^\.]*$\)/\1/'
foo.bar

start cmd:> echo foo.bar.baz | sed 's/\(^.*\)\(\.[^\.]*$\)/\2/'
.baz
share|improve this answer
    
if they don't contain newlines or depending on the echo implementation, don't contain backslashes or are not things like -n or -nene. Also, if the filename doesn't have any dot, both will return the filename. One may also have to consider cases where the file is "foo.d/bar" for instance. –  Stephane Chazelas Jun 27 '13 at 20:25
add comment

I have not seen any standard Unix commands for that. I have used that within shell scripts:

fullname=foo.bar.baz
basename="$(python -c "import os; print os.path.splitext('"$fullname"')[0]")"
extension="$(python -c "import os; print os.path.splitext('"$fullname"')[1]")"

as I find that more easy to get right than regular expressions. YMMV.

share|improve this answer
    
Assuming $fullname doesn't contain any single quote or backslash or newline characters –  Stephane Chazelas Jun 27 '13 at 20:28
    
@StephaneChazelas Yes, in that case you better do the whole thing within Python. –  Anthon Jun 27 '13 at 20:39
add comment

Assuming "foo.bar." is valid output if ext is missing and that there will always be two or three periods, this should work with awk:

basename='foo.bar.baz'
stem=$(echo $basename | awk '{ split($1,a,"."); print a[1] "." a[2]; }')
ext=$(echo $basename | awk '{ split($1,a,"."); print "." a[3]; }');
echo $stem$ext
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.