How do I make the autosuggestions of a zsh function use syntax highlighting - autocomplete

I use zsh and wrote a function to replace cd function. With some help I got it to work like I want it to (mostly). This is a followup to one of my other question.
The function almost works like I want it to, but I still have some problems with syntax highlighting and autocompletion.
For the examples, lets say your directories look like this:
/
a/
b/
c/
d/
some_dir/
I am also assuming the following code has been sourced:
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ -d $first ]; then
pushd $1 >/dev/null # If the first argument is an existing normal directory, move there
else
pushd ${PWD%/$first/*}/$1 >/dev/null # Otherwise, move to a parent directory or a child of that parent directory
fi
}
_cl() {
_cd
pth=${words[2]}
opts=""
new=${pth##*/}
local expl
# Generate the visual formatting and store it in `$expl`
_description -V ancestor-directories expl 'ancestor directories'
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
local ancestor=$PWD:h
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -Q: Don't quote (escape) any of the characters.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -fQ -W "${ancestor:h}/" - "${ancestor:t}"
# Move on to the next parent.
ancestor=$ancestor:h
done
else
# $first is the first part of the path the user typed in.
# it it is part of the current direoctory, we know the user is trying to go back to a directory
first=${pth%%/*}
# $middle is the rest of the provided path
if [ ! -d $first ]; then
# path starts with parent directory
dir=${PWD%/$first/*}/$first
first=$first/
# List all sub directories of the $dir/$middle directory
if [ -d "$dir/$middle" ]; then
for d in $(ls -a $dir/$middle); do
if [ -d $dir/$middle/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
compadd "$expl[#]" -fQ -W $dir/ - $first$middle$d
fi
done
fi
fi
fi
}
compdef _cl cl
The problem:
I use syntax-highlighting, but the path i type is just white (when going to a parent directory. the normal cd functions are colored).
Example:
$ cd /a
$ cl c # 'c' is colored
$ pwd
/a/c
$ cl a/b # 'a/b' is not colored
$ cl a/[tab] # 'a/b', 'a/c' and 'a/some_dir' are not colored
How do I get these paths to be colored?

That's not possible out of the box with the zsh-syntax-highlighting plugin. It checks only whether what you've typed is
A) a valid absolute path or
B) a valid path relative to the present working dir.
This is not specific to your command. Highlighting also fails when specifying path arguments to other commands that are otherwise valid, but are not absolute paths or valid paths relative to the present working dir.
For example:
% cd /a/b
% cd b c # perfectly valid args, but will not get highlighted as valid paths
% pwd
/a/c

Related

Replacing characters in a sh script

I am writing an sh script and need to replace the . and - with a _
Current:
V123_45_678_910.11_1213-1415.sh
Wanted:
V123_45_678_910_11_1213_1415.sh
I have used a few mv commands, but I am having trouble.
for file in /virtualun/rest/scripts/IOL_Extra/*.sh ; do mv $file ${file//V15_IOL_NVMe_01./V15_IOL_NVMe_01_} ; done
You don't need to match any of the other parts of the file name, just the characters you want to replace. To avoid turning foo.sh into foo-sh, remove the extension first, then add it back to the result of the replacement.
for file in /virtualun/rest/scripts/IOL_Extra/*.sh ; do
base=${file%.sh}
mv -i -- "$file" "${base//[-.]/_}".sh
done
Use the -i option to make sure you don't inadvertently replace one file with another when the modified names coincide.
This should work:
#!/usr/bin/env sh
# Fail on error
set -o errexit
# Disable undefined variable reference
set -o nounset
# Enable wildcard character expansion
set +o noglob
# ================
# CONFIGURATION
# ================
# Pattern
PATTERN="/virtualun/rest/scripts/IOL_Extra/*.sh"
# ================
# LOGGER
# ================
# Fatal log message
fatal() {
printf '[FATAL] %s\n' "$#" >&2
exit 1
}
# Info log message
info() {
printf '[INFO ] %s\n' "$#"
}
# ================
# MAIN
# ================
{
# Check directory exists
[ -d "$(dirname "$PATTERN")" ] || fatal "Directory '$PATTERN' does not exists"
for _file in $PATTERN; do
# Skip if not file
[ -f "$_file" ] || continue
info "Analyzing file '$_file'"
# File data
_file_dirname=$(dirname -- "$_file")
_file_basename=$(basename -- "$_file")
_file_name="${_file_basename%.*}"
_file_extension=
case $_file_basename in
*.*) _file_extension=".${_file_basename##*.}" ;;
esac
# New file name
_new_file_name=$(printf '%s\n' "$_file_name" | sed 's/[\.\-][\.\-]*/_/g')
# Skip if equals
[ "$_file_name" != "$_new_file_name" ] || continue
# New file
_new_file="$_file_dirname/${_new_file_name}${_file_extension}"
# Rename
info "Renaming file '$_file' to '$_new_file'"
mv -i -- "$_file" "$_new_file"
done
}
You can try this:
for f in /virtualun/rest/scripts/IOL_Extra/*.sh; do
mv "$f" $(sed 's/[.-]/_/g' <<< "$f")
done
The sed command is replacing all characters .- by _.
I prefer using sed substitute as posted by oliv.
However, if you have not familiar with regular expression, using rename is faster/easier to understand:
Example:
$ touch V123_45_678_910.11_1213-1415.sh
$ rename -va '.' '_' *sh
`V123_45_678_910.11_1213-1415.sh' -> `V123_45_678_910_11_1213-1415_sh'
$ rename -va '-' '_' *sh
`V123_45_678_910_11_1213-1415_sh' -> `V123_45_678_910_11_1213_1415_sh'
$ rename -vl '_sh' '.sh' *sh
`V123_45_678_910_11_1213_1415_sh' -> V123_45_678_910_11_1213_1415.sh'
$ ls *sh
V123_45_678_910_11_1213_1415.sh
Options explained:
-v prints the name of the file before -> after the operation
-a replaces all occurrences of the first argument with the second argument
-l replaces the last occurrence of the first argument with the second argument
Note that this might not be suitable depending on the other files you have in the given directory that would match *sh and that you do NOT want to rename.

How do I make a zsh function autocomplet from the middle of a word?

I use zsh and wrote a function to replace cd function. With some help I got it to work like I want it to (mostly). This is a followup to one of my other question.
The function almost works like I want it to, but I still have some problems with syntax highlighting and autocompletion.
For the examples, lets say your directories look like this:
/
a/
b/
c/
d/
some_dir/
I am also assuming the following code has been sourced:
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ -d $first ]; then
pushd $1 >/dev/null # If the first argument is an existing normal directory, move there
else
pushd ${PWD%/$first/*}/$1 >/dev/null # Otherwise, move to a parent directory or a child of that parent directory
fi
}
_cl() {
_cd
pth=${words[2]}
opts=""
new=${pth##*/}
local expl
# Generate the visual formatting and store it in `$expl`
_description -V ancestor-directories expl 'ancestor directories'
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
local ancestor=$PWD:h
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -Q: Don't quote (escape) any of the characters.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -fQ -W "${ancestor:h}/" - "${ancestor:t}"
# Move on to the next parent.
ancestor=$ancestor:h
done
else
# $first is the first part of the path the user typed in.
# it it is part of the current direoctory, we know the user is trying to go back to a directory
first=${pth%%/*}
# $middle is the rest of the provided path
if [ ! -d $first ]; then
# path starts with parent directory
dir=${PWD%/$first/*}/$first
first=$first/
# List all sub directories of the $dir/$middle directory
if [ -d "$dir/$middle" ]; then
for d in $(ls -a $dir/$middle); do
if [ -d $dir/$middle/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
compadd "$expl[#]" -fQ -W $dir/ - $first$middle$d
fi
done
fi
fi
fi
}
compdef _cl cl
The problem:
In my zshrc, I have a line:
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' '+l:|=* r:|=*'
This should make autocompletions case insensitive and make sure I can start typing the last part of a directory name, and in will still finnish the full name
Example:
$ cd /a
$ cd di[tab] # replaces 'di' with 'some_dir/'
$ cl di[tab] # this does not do anything. I would like it to replace 'di' with 'some_dir'
How do get it to suggest 'some_dir' when I type 'di'?
The second matcher in your matcher-list never gets called, because _cl() returns "true" (exit status 0, actually) even when it has not added any matches. Returning "true" causes _main_complete() to assume that we're done completing and it will thus not try the next matcher in the list.
To fix this, add the following to the start of _cl():
local -i nmatches=$compstate[nmatches]
and this to the end of _cl():
(( compstate[nmatches] > nmatches ))
That way, _cl() will return "true" only when it has managed to actually add completions.

can't use '~' in zsh autocompletion

I use zsh and I want to use a function I wrote to replace cd.
This function gives you the ability to move to a parent directory:
$ pwd
/a/b/c/d
$ cl b
$ pwd
/a/b
You can also move into a subdirectory of a parent directory:
$ pwd
/a/b/c/d
$ cl b/e
$ pwd
/a/b/e
If the first part of the path is not a parent directory, it will just function as normal cd would. I hope that makes sense.
In summary, when in /a/b/c/d, I want to be able to move to /a, /a/b, /a/b/c, all subdirectories of /a/b/c/d and any absolute path starting with /, ~/ or ../ (or ./).
I hope that makes sense.
This is the function I wrote:
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ $# -eq 0 ]; then
# cl without any arguments moves back to the previous directory
cd - > /dev/null
elif [ -d $first ]; then
# If the first argument is an existing normal directory, move there
cd $1
else
# Otherwise, move to a parent directory
cd ${PWD%/$first/*}/$1
fi
}
There is probably a better way to this (tips are welcome), but I haven't had any problems with this so far.
Now I want to add autocompletion. This is what I have so far:
_cl() {
pth=${words[2]}
opts=""
new=${pth##*/}
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
opts+=" "
first=""
d="${${PWD#/}%/*}/"
opts+="${d//\/// }"
dir=$PWD
else
first=${pth%%/*}
if [[ "$first" == "" ]]; then
# path starts with "/"
dir="/$middle"
elif [[ "$first" == "~" ]]; then
# path starts with "~/"
dir="$HOME/$middle"
elif [ -d $first ]; then
# path starts with a directory in the current directory
dir="$PWD/$first/$middle"
else
# path starts with parent directory
dir=${PWD%/$first/*}/$first/$middle
fi
first=$first/
fi
# List al sub directories of the $dir directory
if [ -d "$dir" ]; then
for d in $(ls -a $dir); do
if [ -d $dir/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
opts+="$first$middle$d/ "
fi
done
fi
_multi_parts / "(${opts})"
return 0
}
compdef _cl cl
Again, probably not the best way to do this, but it works... kinda.
One of the problems is that what I type cl ~/, it replaces it with cl ~/ and does not suggest any directories in my home folder. Is there a way to get this to work?
EDIT
cl () {
local first=$( echo $1 | cut -d/ -f1 )
if [ $# -eq 0 ]; then
# cl without any arguments moves back to the previous directory
local pwd_bu=$PWD
[[ $(dirs) == "~" ]] && return 1
while [[ $PWD == $pwd_bu ]]; do
popd >/dev/null
done
local pwd_nw=$PWD
[[ $(dirs) != "~" ]] && popd >/dev/null
pushd $pwd_bu >/dev/null
pushd $pwd_nw >/dev/null
elif [ -d $first ]; then
pushd $1 >/dev/null # If the first argument is an existing normal directory, move there
else
pushd ${PWD%/$first/*}/$1 >/dev/null # Otherwise, move to a parent directory or a child of that parent directory
fi
}
_cl() {
_cd
pth=${words[2]}
opts=""
new=${pth##*/}
local expl
# Generate the visual formatting and store it in `$expl`
_description -V ancestor-directories expl 'ancestor directories'
[[ "$pth" != *"/"*"/"* ]] && middle="" || middle="${${pth%/*}#*/}/"
if [[ "$pth" != *"/"* ]]; then
# If this is the start of the path
# In this case we should also show the parent directories
local ancestor=$PWD:h
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -Q: Don't quote (escape) any of the characters.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -fQ -W "${ancestor:h}/" - "${ancestor:t}"
# Move on to the next parent.
ancestor=$ancestor:h
done
else
# $first is the first part of the path the user typed in.
# it it is part of the current direoctory, we know the user is trying to go back to a directory
first=${pth%%/*}
# $middle is the rest of the provided path
if [ ! -d $first ]; then
# path starts with parent directory
dir=${PWD%/$first/*}/$first
first=$first/
# List all sub directories of the $dir/$middle directory
if [ -d "$dir/$middle" ]; then
for d in $(ls -a $dir/$middle); do
if [ -d $dir/$middle/$d ] && [[ "$d" != "." ]] && [[ "$d" != ".." ]]; then
compadd "$expl[#]" -fQ -W $dir/ - $first$middle$d
fi
done
fi
fi
fi
}
compdef _cl cl
This is as far as I got on my own. It does works (kinda) but has a couple of problems:
When going back to a parent directory, completion mostly works. But when you go to a child of the paretn directory, the suggestions are wrong (they display the full path you have typed, not just the child directory). The result does work
I use syntax-hightlighting, but the path I type is just white (when using going to a parent directory. the normal cd functions are colored)
In my zshrc, I have the line:
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' '+l:|=* r:|=*'
Whith cd this means I can type "load" and it will complete to "Downloads". With cl, this does not work. Not event when using the normal cd functionality.
Is there a way to fix (some of these) problems?
I hope you guys understand my questions. I find it hard to explain the problem.
Thanks for your help!
This should do it:
_cl() {
# Store the number of matches generated so far.
local -i nmatches=$compstate[nmatches]
# Call the built-in completion for `cd`. No need to reinvent the wheel.
_cd
# ${PWD:h}: The parent ("head") of the present working dir.
local ancestor=$PWD:h expl
# Generate the visual formatting and store it in `$expl`
# -V: Don't sort these items; show them in the order we add them.
_description -V ancestor-directories expl 'ancestor directories'
while (( $#ancestor > 1 )); do
# -f: Treat this as a file (incl. dirs), so you get proper highlighting.
# -W: Specify the parent of the dir we're adding.
# ${ancestor:h}: The parent ("head") of $ancestor.
# ${ancestor:t}: The short name ("tail") of $ancestor.
compadd "$expl[#]" -f -W ${ancestor:h}/ - $ancestor:t
# Move on to the next parent.
ancestor=$ancestor:h
done
# Return true if we've added any matches.
(( compstate[nmatches] > nmatches ))
}
# Define the function above as generating completions for `cl`.
compdef _cl cl
# Alternatively, instead of the line above:
# 1. Create a file `_cl` inside a dir that's in your `$fpath`.
# 2. Paste the _contents_ of the function `_cl` into this file.
# 3. Add `#compdef cl` add the top of the file.
# `_cl` will now get loaded automatically when you run `compinit`.
Also, I would rewrite your cl function like this, so it no longer depends on cut or other external commands:
cl() {
if (( $# == 0 )); then
# `cl` without any arguments moves back to the previous directory.
cd -
elif [[ -d $1 || -d $PWD/$1 ]]; then
# If the argument is an existing absolute path or direct child, move there.
cd $1
else
# Get the longest prefix that ends with the argument.
local ancestor=${(M)${PWD:h}##*$1}
if [[ -d $ancestor ]]; then
# Move there, if it's an existing dir.
cd $ancestor
else
# Otherwise, print to stderr and return false.
print -u2 "$0: no such ancestor '$1'"
return 1
fi
fi
}
Alternative Solution
There is an easier way to do all of this, without the need to write a cd replacement or any completion code:
cdpath() {
# `$PWD` is always equal to the present working directory.
local dir=$PWD
# In addition to searching all children of `$PWD`, `cd` will also search all
# children of all of the dirs in the array `$cdpath`.
cdpath=()
# Add all ancestors of `$PWD` to `$cdpath`.
while (( $#dir > 1 )); do
# `:h` is the direct parent.
dir=$dir:h
cdpath+=( $dir )
done
}
# Run the function above whenever we change directory.
add-zsh-hook chpwd cdpath
Zsh's completion code for cd automatically takes $cdpath into account. No need to even configure that. :)
As an example of how this works, let's say you're in /Users/marlon/.zsh/prezto/modules/history-substring-search/external/.
You can now type cd pre and press Tab, and Zsh will complete it to cd prezto. After that, pressing Enter will take you directly to /Users/marlon/.zsh/prezto/.
Or let's say that there also exists /Users/marlon/.zsh/prezto/modules/prompt/external/agnoster/. When you're in the former dir, you can do cd prompt/external/agnoster to go directly to the latter, and Zsh will complete this path for you every step of the way.

How to remove YAML frontmatter from markdown files?

I have markdown files that contain YAML frontmatter metadata, like this:
---
title: Something Somethingelse
author: Somebody Sometheson
---
But the YAML is of varying widths. Can I use a Posix command like sed to remove that frontmatter when it's at the beginning of a file? Something that just removes everything between --- and ---, inclusive, but also ignores the rest of the file, in case there are ---s elsewhere.
I understand your question to mean that you want to remove the first ----enclosed block if it starts at the first line. In that case,
sed '1 { /^---/ { :a N; /\n---/! ba; d} }' filename
This is:
1 { # in the first line
/^---/ { # if it starts with ---
:a # jump label for looping
N # fetch the next line, append to pattern space
/\n---/! ba; # if the result does not contain \n--- (that is, if the last
# fetched line does not begin with ---), go back to :a
d # then delete the whole thing.
}
}
# otherwise drop off the end here and do the default (print
# the line)
Depending on how you want to handle lines that begin with ---abc or so, you may have to change the patterns a little (perhaps add $ at the end to only match when the whole line is ---). I'm a bit unclear on your precise requirements there.
If you want to remove only the front matter, you could simply run:
sed '1{/^---$/!q;};1,/^---$/d' infile
If the first line doesn't match ---, sed will quit; else it will delete everything from the 1st line up to (and including) the next line matching --- (i.e. the entire front matter).
If you don't mind the "or something" being perl.
Simply print after two instances of "---" have been found:
perl -ne 'if ($i > 1) { print } else { /^---/ && $i++ }' yaml
or a bit shorter if you don't mind abusing ?: for flow control:
perl -ne '$i > 1 ? print : /^---/ && $i++' yaml
Be sure to include -i if you want to replace inline.
you use a bash file, create script.sh and make it executable using chmod +x script.sh and run it ./script.sh.
#!/bin/bash
#folder articles contains a lot of markdown files
files=./articles/*.md
for f in $files;
do
#filename
echo "${f##*/}"
#replace frontmatter title attribute to "title"
sed -i -r 's/^title: (.*)$/title: "\1"/' $f
#...
done
This AWK based solution works for files with and without FrontMatter, doing nothing in the later case.
#!/bin/sh
# Strips YAML FrontMattter from a file (usually Markdown).
# Exit immediately on each error and unset variable;
# see: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -Ee
print_help() {
echo "Strips YAML FrontMattter from a file (usually Markdown)."
echo
echo "Usage:"
echo " `basename $0` -h"
echo " `basename $0` --help"
echo " `basename $0` -i <file-with-front-matter>"
echo " `basename $0` --in-place <file-with-front-matter>"
echo " `basename $0` <file-with-front-matter> <file-to-be-without-front-matter>"
}
replace=false
in_file="-"
out_file="/dev/stdout"
if [ -n "$1" ]
then
if [ "$1" = "-h" ] || [ "$1" = "--help" ]
then
print_help
exit 0
elif [ "$1" = "-i" ] || [ "$1" = "--in-place" ]
then
replace=true
in_file="$2"
out_file="$in_file"
else
in_file="$1"
if [ -n "$2" ]
then
out_file="$2"
fi
fi
fi
tmp_out_file="$out_file"
if $replace
then
tmp_out_file="${in_file}_tmp"
fi
awk -e '
BEGIN {
is_first_line=1;
in_fm=0;
}
/^---$/ {
if (is_first_line) {
in_fm=1;
}
}
{
if (! in_fm) {
print $0;
}
}
/^(---|...)$/ {
if (! is_first_line) {
in_fm=0;
}
is_first_line=0;
}
' "$in_file" >> "$tmp_out_file"
if $replace
then
mv "$tmp_out_file" "$out_file"
fi

How do you climb up the parent directory structure of a bash script?

Is there a neater way of climbing up multiple directory levels from the location of a script.
This is what I currently have.
# get the full path of the script
D=$(cd ${0%/*} && echo $PWD/${0##*/})
D=$(dirname $D)
D=$(dirname $D)
D=$(dirname $D)
# second level parent directory of script
echo $D
I would like a neat way of finding the nth level. Any ideas other than putting in a for loop?
dir="/path/to/somewhere/interesting"
saveIFS=$IFS
IFS='/'
parts=($dir)
IFS=$saveIFS
level=${parts[3]}
echo "$level" # output: somewhere
#!/bin/sh
ancestor() {
local n=${1:-1}
(for ((; n != 0; n--)); do cd $(dirname ${PWD}); done; pwd)
}
Usage:
$ pwd
/home/nix/a/b/c/d/e/f/g
$ ancestor 3
/home/nix/a/b/c/d
A solution without loops would be to use recursion. I wanted to find a config file for a script by traversing backwards up from my current working directory.
rtrav() { test -e $2/$1 && echo $2 || { test $2 != / && rtrav $1 `dirname $2`;}; }
To check if the current directory is in a GIT repo: rtrav .git $PWD
rtrav will check the existence of a filename given by the first argument in each parent folder of the one given as the second argument. Printing the directory path where the file was found or exiting with an error code if the file was not found.
The predicate (test -e $2/$1) could be swapped for checking of a counter that indicates the traversal depth.
If you're OK with including a Perl command:
$ pwd
/u1/myuser/dir3/dir4/dir5/dir6/dir7
The first command lists the directory containing first N (in my case 5) directories
$ perl-e 'use File::Spec; \
my #dirs = File::Spec->splitdir( \
File::Spec->rel2abs( File::Spec->curdir() ) ); \
my #dirs2=#dirs[0..5]; print File::Spec->catdir(#dirs2) . "\n";'
/u1/myuser/dir3/dir4/dir5
The second command lists the directory N levels up (in my case 5) directories (I think you wanted the latter).
$ perl -e 'use File::Spec; \
my #dirs = File::Spec->splitdir( \
File::Spec->rel2abs( File::Spec->curdir() ) ); \
my #dirs2=#dirs[0..$#dir-5]; print File::Spec->catdir(#dirs2)."\n";'
/u1/myuser
To use it in your bash script, of course:
D=$(perl -e 'use File::Spec; \
my #dirs = File::Spec->splitdir( \
File::Spec->rel2abs( File::Spec->curdir() ) ); \
my #dirs2=#dirs[0..$#dir-5]; print File::Spec->catdir(#dirs2)."\n";')
Any ideas other than putting in a for loop?
In shells, you can't avoid the loop, because traditionally they do not support regexp, but glob matching instead. And glob patterns do not support the any sort of repeat counters.
And BTW, simplest way is to do it in shell is: echo $(cd $PWD/../.. && echo $PWD) where the /../.. makes it strip two levels.
With tiny bit of Perl that would be:
perl -e '$ENV{PWD} =~ m#^(.*)(/[^/]+){2}$# && print $1,"\n"'
The {2} in the Perl's regular expression is the number of directory entries to strip. Or making it configurable:
N=2
perl -e '$ENV{PWD} =~ m#^(.*)(/[^/]+){'$N'}$# && print $1,"\n"'
One can also use Perl's split(), join() and splice() for the purpose, e.g.:
perl -e '#a=split("/", $ENV{PWD}); print join("/", splice(#a, 0, -2)),"\n"'
where -2 says that from the path the last two entries has to be removed.
Two levels above the script directory:
echo "$(readlink -f -- "$(dirname -- "$0")/../..")"
All the quoting and -- are to avoid problems with tricky paths.
This method uses the actual full path to the perl script itself ... TIMTOWTDI
You could just easily replace the $RunDir with the path you would like to start with ...
#resolve the run dir where this scripts is placed
$0 =~ m/^(.*)(\\|\/)(.*)\.([a-z]*)/;
$RunDir = $1 ;
#change the \'s to /'s if we are on Windows
$RunDir =~s/\\/\//gi ;
my #DirParts = split ('/' , $RunDir) ;
for (my $count=0; $count < 4; $count++) { pop #DirParts ; }
$confHolder->{'ProductBaseDir'} = $ProductBaseDir ;
This allows you to work your way up until whatever condition is desired
WORKDIR=$PWD
until test -d "$WORKDIR/infra/codedeploy"; do
# get the full path of the script
WORKDIR=$(dirname $WORKDIR)
done