Regex-style path extraction tricks in Linux Shell

When you write shell scripts, one of the most common tasks is extracting parts of a path: the filename, the directory, the extension, or a parent folder. It looks small, but a single mistake can cause a deployment script to target the wrong folder or write files to the wrong location.

Assume a file path:

file=/dir1/dir2/dir3/my.file.txt

We can use ${ } parameter expansion to obtain different values.

Extract from the left (using #)

# deletes the shortest matching prefix (from the left, non-greedy), while ## deletes the longest matching prefix (greedy):

# Remove the first / and everything left of it → dir1/dir2/dir3/my.file.txt
echo ${file#*/}

# Remove the last / and everything left of it → my.file.txt
echo ${file##*/}

# Remove the first . and everything left of it → file.txt
echo ${file#*.}

# Remove the last . and everything left of it → txt
echo ${file##*.}

Extract from the right (using %)

% deletes the shortest matching suffix (from the right, non-greedy), while %% deletes the longest matching suffix (greedy):

# Remove the last / and everything right of it → /dir1/dir2/dir3
echo ${file%/*}

# Remove the first / and everything right of it → (empty)
echo ${file%%/*}

# Remove the last . and everything right of it → /dir1/dir2/dir3/my.file
echo ${file%.*}

# Remove the first . and everything right of it → /dir1/dir2/dir3/my
echo ${file%%.*}

Result comparison table

With file=/dir1/dir2/dir3/my.file.txt:

Expression Result
${file#*/} dir1/dir2/dir3/my.file.txt
${file##*/} my.file.txt
${file#*.} file.txt
${file##*.} txt
${file%/*} /dir1/dir2/dir3
${file%%/*} (empty)
${file%.*} /dir1/dir2/dir3/my.file
${file%%.*} /dir1/dir2/dir3/my

Memory tips

  • Look at the symbol direction: # starts trimming from the left; % trims from the right;
  • Look at the symbol count: a single symbol means “shortest match” (non-greedy), double symbols mean “longest match” (greedy);
  • Common uses: ${file##*/} gets the filename, ${file%/*} gets the directory, ${file%.*} strips the last extension, and ${file##*.} gets the extension.

Practical example

Extract the path, filename, and extension of a file in a script:

file=/var/www/html/index.php

dir=$(dirname "$file")        # /var/www/html
base=$(basename "$file")      # index.php
name=${file%.*}               # /var/www/html/index
ext=${file##*.}               # php

A deployment-oriented example

Suppose you are writing a deployment script that extracts the archive name and creates a folder for it:

archive=/data/releases/app-v1.2.3.tar.gz
base_dir=${archive%.*}
package_name=${archive##*/}
extension=${archive##*.}

mkdir -p "/data/releases/${base_dir##*/}"
echo "Package: $package_name"
echo "Extension: $extension"

A real deployment scenario: generating a target directory from an archive name

In a real release workflow, you usually deal with more than a single file. Suppose you upload an archive named /data/releases/app-service-2026.08.01.tar.gz to a server and want to extract it into a directory matching the version label while keeping the logs in the same place. This is a good case for separating the filename, parent directory, and suffix from the path:

archive=/data/releases/app-service-2026.08.01.tar.gz
version=${archive##*/}
base_name=${version%.tar.gz}
release_dir=${archive%/*}
target_dir="$release_dir/$base_name"

mkdir -p "$target_dir"
echo "Extract directory: $target_dir"

This example highlights a few practical points: first, ${archive##*/} extracts the archive name; second, ${version%.tar.gz} removes the archive suffix so the directory name is cleaner; third, ${archive%/*} keeps the parent path flexible instead of hard-coding it. That approach is reliable in CI/CD publishing, rollback jobs, and log archiving.

Troubleshooting tip: print the intermediate values

If a script behaves unexpectedly, the first thing to do is print the intermediate values before changing the logic. This is especially useful when a directory name looks wrong or an extension is extracted incorrectly:

archive=/data/releases/app-service-2026.08.01.tar.gz
echo "Original path: $archive"
echo "Filename: ${archive##*/}"
echo "Parent directory: ${archive%/*}"
echo "Without suffix: ${archive%.tar.gz}"
echo "Extension: ${archive##*.}"

This makes it easier to tell whether the issue comes from the path format, a dot in the filename, or a suffix that was matched differently.

This path-extraction technique is very useful when writing deployment scripts and log-processing scripts. For more shell scripting and server operations content, see the developer tools category, such as Linux server basics and server operations tips.

A practical combination with dirname and basename

In real scripts, path extraction is easier to maintain when it is paired with helper commands such as dirname and basename. These tools make it clearer how you are deriving a target folder, a file name, or a deployment path. For example, if you want to unpack an archive into a directory named after the package, you can extract the archive name and parent folder before creating the destination directory.

archive=/opt/releases/app-2026.08.01.tar.gz
folder=$(basename "$archive" .tar.gz)
workdir=$(dirname "$archive")

mkdir -p "$workdir/$folder"
echo "Target directory: $workdir/$folder"

Common pitfalls: trailing slashes and empty strings

Another common shell path issue is the trailing slash. Paths such as /var/www/html/ and /var/www/html may look similar, but in scripts they can produce different results when you concatenate them with other paths or when a command treats the empty string unexpectedly. A simple normalization step helps make the script more robust.

path="/var/www/html/"
path=${path%/}
[ -d "$path" ] && echo "Directory exists"

This style of scripting is common in deployment automation, log archiving, and site publishing workflows because it is easier for the next maintainer to understand than a chain of brittle string operations.

When it is worth wrapping path handling into a function

As you write more scripts, path handling logic starts to appear repeatedly. Some scripts need the directory name, others need the file name, and some need the extension or a version segment. At that point, wrapping the common logic into a small function is easier to maintain than repeatedly rewriting parameter expansion. A simple example is extracting the directory, basename, and extension through one reusable entry point.

get_path_parts() {
    local path="$1"
    echo "dir=${path%/*}"
    echo "base=${path##*/}"
    echo "ext=${path##*.}"
}

get_path_parts "/var/www/html/index.php"

This kind of small wrapper may not look impressive, but it reduces maintenance cost significantly as scripts evolve.

Original post: https://www.cnblogs.com/cqzhuomi/articles/17284324.html (cnblogs.com CQZHUOMI, repost)