Alby
Alby

Reputation: 5752

Get the multilevel basename of a Path

I am trying to write a program that is sort of similar to UNIX basename, except I can control the level of its base.

For example, the program would perform tasks like the following:

$PROGRAM /PATH/TO/THE/FILE.txt 1
FILE.txt # returns the first level basename

$PROGRAM /PATH/TO/THE/FILE.txt 2
THE/FILE.txt #returns the second level basename

$ PROGRAM /PATH/TO/THE/FILE.txt 3
TO/THE/FILE.txt #returns the third level base name

I was trying to write this in perl, and to quickly test my idea, I used the following command line script to obtain the second level basename, to no avail:

$echo "/PATH/TO/THE/FILE.txt" | perl -ne '$rev=reverse $_; $rev=~s:((.*?/){2}).*:$2:; print scalar reverse $rev'
/THE

As you can see, it's only printing out the directory name and not the rest. I feel this has to do with nongreedy matching with quantifier or what not, but my knowledge lacks in that area.

If there is more efficient way to do this in bash, please advise

Upvotes: 2

Views: 1238

Answers (5)

Borodin
Borodin

Reputation: 126742

You will find that your own solution works fine if you use $1 in the substitution instead of $2. The captures are numbered in the order that their opening parentheses appear within the regex, and you want to retain the outermost capture. However the code is less than elegant.

The File::Spec module is ideal for this purpose. It has been a core module with every release of Perl v5 and so shouldn't need installing.

use strict;
use warnings;

use File::Spec;

my @path = File::Spec->splitdir($ARGV[0]);
print File::Spec->catdir(splice @path, -$ARGV[1]), "\n";

output

E:\Perl\source>bnamen.pl /PATH/TO/THE/FILE.txt 1
FILE.txt

E:\Perl\source>bnamen.pl /PATH/TO/THE/FILE.txt 2
THE\FILE.txt

E:\Perl\source>bnamen.pl /PATH/TO/THE/FILE.txt 3
TO\THE\FILE.txt

Upvotes: 2

gniourf_gniourf
gniourf_gniourf

Reputation: 46853

A pure bash solution (with no checking of the number of arguments and all that):

#!/bin/bash

IFS=/ read -a a <<< "$1"
IFS=/ scratch="${a[*]:${#a[@]}-$2}"
echo "$scratch"

Done.

Works like this:

$ ./program /PATH/TO/THE/FILE.txt 1
FILE.txt
$ ./program /PATH/TO/THE/FILE.txt 2
THE/FILE.txt
$ ./program /PATH/TO/THE/FILE.txt 3
TO/THE/FILE.txt
$ ./program /PATH/TO/THE/FILE.txt 4
PATH/TO/THE/FILE.txt

Upvotes: 3

reo katoa
reo katoa

Reputation: 5801

As @tripleee said, split on the path delimiter ("/" for Unix-like) and then paste back together. For example:

echo "/PATH/TO/THE/FILE.txt" | perl -ne 'BEGIN{$n=shift} @p = split /\//; $start=($#p-$n+1<0?0:$#p-$n+1); print join("/",@p[$start..$#p])' 1
FILE.txt

echo "/PATH/TO/THE/FILE.txt" | perl -ne 'BEGIN{$n=shift} @p = split /\//; $start=($#p-$n+1<0?0:$#p-$n+1); print join("/",@p[$start..$#p])' 3
TO/THE/FILE.txt

Just for fun, here's one that will work on Unix and Windows (and any other) path types, if you provide the delimiter as the second argument:

# Unix-like
echo "PATH/TO/THE/FILE.txt" | perl -ne 'BEGIN{$n=shift;$d=shift} @p = split /\Q$d\E/; $start=($#p-$n+1<0?0:$#p-$n+1); print join($d,@p[$start..$#p])' 3 /
TO/THE/FILE.txt
# Wrong delimiter
echo "PATH/TO/THE/FILE.txt" | perl -ne 'BEGIN{$n=shift;$d=shift} @p = split /\Q$d\E/; $start=($#p-$n+1<0?0:$#p-$n+1); print join($d,@p[$start..$#p])' 3 \\
PATH/TO/THE/FILE.txt
# Windows
echo "C:\Users\Name\Documents\document.doc" | perl -ne 'BEGIN{$n=shift;$d=shift} @p = split /\Q$d\E/; $start=($#p-$n+1<0?0:$#p-$n+1); print join($d,@p[$start..$#p])' 3 \\
Name\Documents\document.doc
# Wrong delimiter
echo "C:\Users\Name\Documents\document.doc" | perl -ne 'BEGIN{$n=shift;$d=shift} @p = split /\Q$d\E/; $start=($#p-$n+1<0?0:$#p-$n+1); print join($d,@p[$start..$#p])' 3 /
C:\Users\Name\Documents\document.doc

Upvotes: 1

sampson-chen
sampson-chen

Reputation: 47327

Here's a bash script to do it with awk:

#!/bin/bash

level=$1
awk -v lvl=$level 'BEGIN{FS=OFS="/"}
    {count=NF-lvl+1;
    if (count < 1) {
        count=1;
    }
    while (count <= NF) {
        if (count > NF-lvl+1 ) {
            printf "%s", OFS;
        }
        printf "%s", $(count);
        count+=1;
    }
    printf "\n";
}'

To use it, do:

$ ./script_name num_args input_file

For example, if file input contains the line "/PATH/TO/THE/FILE.txt"

$ ./get_lvl_name 2 < input
THE/FILE.txt
$

Upvotes: 1

dimir
dimir

Reputation: 793

#!/bin/bash                                                                                                                    

[ $# -ne 2 ] && exit

input=$1
rdepth=$2
delim=/

[ $rdepth -lt 1 ] && echo "depth must be greater than zero" && exit

parts=$(echo -n $input | sed "s,[^$delim],,g" | wc -m)
[ $parts -lt 1 ] && echo "invalid path" && exit

[ $rdepth -gt $parts ] && echo "input has only $parts part(s)" && exit

depth=$((parts-rdepth+2))

echo $input | cut -d "$delim" -f$depth-

Usage:

$ ./level.sh /tmp/foo/bar 2
foo/bar

Upvotes: 1

Related Questions