21

How do you do it? My directory code/, at work, is organized in folders and subfolders and subsubfolders, all of which (at least in theory) contain scripts or programs I want to run on a regular basis.

nbro
  • 15,395
  • 32
  • 113
  • 196
user50264
  • 535
  • 3
  • 6
  • 14

9 Answers9

22

At the end of your script, put the line:

PATH=${PATH}:$(find ~/code -type d | tr '\n' ':' | sed 's/:$//')

This will append every directory in your ~/code tree to the current path. I don't like the idea myself, preferring to have only a couple of directories holding my own executables and explicitly listing them, but to each their own.

If you want to exclude all directories which are hidden, you basically need to strip out every line that has the sequence "/." (to ensure that you don't check subdirectories under hidden directories as well):

PATH=${PATH}:$(find ~/code -type d | sed '/\/\\./d' | tr '\n' ':' | sed 's/:$//')

This will stop you from getting directories such as ~/code/level1/.hidden/level3/ (i.e., it stops searching within sub-trees as soon as it detects they're hidden). If you only want to keep the hidden directories out, but still allow non-hidden directories under them, use:

PATH=${PATH}:$(find ~/code -type d -name '[^\.]*' | tr '\n' ':' | sed 's/:$//')

This would allow ~/code/level1/.hidden2/level3/ but disallow ~/code/level1/.hidden2/.hidden3/ since -name only checks the base name of the file, not the full path name.

SpinUp __ A Davis
  • 5,016
  • 1
  • 30
  • 25
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • Nice, but same comment as for Vardhan... that is I need to exclude hidden directories. Also, what is the purpose of the piping the result of tr to sed? – user50264 Mar 18 '09 at 06:11
  • hes trimming the last : so it doesn't produce a bogus path – Kent Fredric Mar 18 '09 at 06:13
  • 1
    you'd probably want to use find -print0 | tr "\0" ":" there instead Pax, then you'll have a filter that should work on *ALL* valid unix paths ( including ones with pesky newlines in them ) – Kent Fredric Mar 18 '09 at 06:29
  • 3
    @Kent: Non-printable characters (including spaces) are evil and, if I ever find someone who uses them, I'll beat them to death with my entire collection of IBM mainframe COBOL manuals :-) – paxdiablo Mar 18 '09 at 06:36
  • find -type d -print0 | sed -e "y/\d0/:/;s/:$//;" # also another reduction possible. – Kent Fredric Mar 18 '09 at 06:37
  • 1
    if you want entertainment, put a beep character into a directory name. You'll know if somebody in the room CD's into it that way ;) – Kent Fredric Mar 18 '09 at 06:38
  • @xxlunatic, see update for stripping out hidden directories (and all their subdirectories). – paxdiablo Mar 18 '09 at 06:42
  • @Pax, why not just tell find to strip out hidden directories rather than doing it after-the-fact? – Charles Duffy Mar 18 '09 at 07:22
  • Because if you have a directory ~/code/.hidden/not_hidden, the find will give it to you with -name '[^\.]*' - I assumed xxlunatic wanted to stop searching a subtree as soon as it was deemed hidden. – paxdiablo Mar 18 '09 at 07:30
  • @Pax, using -prune will stop find from recursing down those directories in the first place. – Charles Duffy Mar 18 '09 at 14:51
  • This solution is awful. It isn't robust against filenames with newlines (doesn't matter if they're evil, they exist in the wild -- and can be used in attempted security exploits against your scripts) and is unnecessarily inefficient (using `sed` where functionality built into `find` would work just as well). Very disappointed to see it marked accepted. – Charles Duffy Feb 28 '11 at 02:38
  • 1
    @Charles, then downvote it and upvote another one. There's a difference between 'accepted' (best solved the questioner's problem) and 'highest voted' (which, I hope, most look at as the best as voted by the community). In fact, I've raised a request before to have a view where the highest voted comes _above_ the accepted answer, but it was knocked back. That wouldn't help you here of course since this answer is both highest voted _and_ accepted but that may not be the case forever. – paxdiablo Feb 28 '11 at 06:04
  • See http://meta.stackexchange.com/questions/60777/modified-answer-view-based-only-on-votes-not-accepted-status - well, not really knocked back, but not actioned, which amounts to the same thing :-) But I'm deadly serious about you downvoting me. If you think it's not helpful, I'd rather you do that. I won't be offended, or affected by losing 2 rep :-) , and it'll hopefully lead to a better answer (assuming your opinion is right, I won't comment on that) rising above it in votes. – paxdiablo Feb 28 '11 at 06:09
18

The following Does The Right Thing, including trimming hidden directories and their children and properly handling names with newlines or other whitespace:

export PATH="${PATH}$(find ~/code -name '.*' -prune -o -type d -printf ':%p')"

I use a similar trick for automatically setting CLASSPATHs.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 2
    Very cool, no invocations sed because find handles everything for you... except I have a version of find without printf :-/ – user50264 Mar 19 '09 at 00:50
2

Something like

my_path=$(find $root -type d | tr '\n' ':')

or

my_path=$(find $root -type d -printf '%p:')
Paul Fleming
  • 24,238
  • 8
  • 76
  • 113
vrdhn
  • 4,024
  • 3
  • 31
  • 39
  • Very. Nice. Any way to exclude hidden directories? I'm have a git repo, so trying it out I got many more directories than I was expecting :-/ – user50264 Mar 18 '09 at 06:00
  • 1
    No reason to have find print with '\n's -- better to tell it to colon-separate the files in the first place. – Charles Duffy Mar 18 '09 at 07:27
  • @user50264 - the same way you'd do any kind of exclusion with git; for instance: `find "$root" -name ".[a-z]*" -prune -o -type d -printf '%p:'` to exclude dotfiles matching the given pattern (excluding `.` is unwise, and the given hack is a passable 80% solution to avoid it). – Charles Duffy Feb 28 '11 at 02:35
2

If you really need to go down this road, you could try minimizing that PATHs list some more: drop folders that contain no executables. Of course, at the cost of even more stats. ;-/

PATH=$PATH$(find ~/code -name '.*' -prune -o -type f -a -perm /u+x -printf ':%h\n' | sort | uniq | tr -d '\n')

I'd avoid doing this at each shell spawn. Some kind of caching should be used. For instance, add this line to your ~/.bashrc:

[ -s ~/.codepath ] && export PATH=$PATH$(<~/.codepath)

and run

find ~/code -name '.*' -prune -o -type f -a -perm /u+x -printf ':%h\n' |sort |uniq |tr -d '\n' > ~/.codepath

only when you know something really changed.

EDIT: here's a rewrite without your missing -printf

find ~/code -name '.*' -prune -o -type f -a -perm /u+x -print | sed 's@/[^/]\+$@:@' | sort | uniq | tr -d '\n' | sed 's/^/:/; s/:$//'
altblue
  • 937
  • 12
  • 19
1

In bash 4.0 you can just use the newly supported ** operator.

You have to enable it first on some with :

shopt -s globstar

You can then do

echo ** 

which recursively echos all files that are descendant of the current dir.

Beware it does tend to bail out on overly complicated dirs sometimes, so use the ** at the lowest recucurring point.

echo **/  

Coincidentally, emits recursively all directory names, and only directory names. ( Excluding the current dir )

echo ./**/ 

Includes the current dir. ( Incidentally, it also skips hidden directories )

This should thuswise be suited for creating a path string:

echo ./**/ | sed 's/\s\s*/:/g'

And if you don't want relative paths,

echo $PWD/**/ | sed 's/\s\s*/:/g' 

Ack

From your comment on one of the other posts it sounds like you're wanting behaviour much like 'Ack' provides. If you were intending to use a find + grep combination, this tool is generally much more efficient and easier to use for this task.

Example:

# search for 'mystring' in all c++ files recursively ( excluding SCM dirs and backup files ) 
ack  "mystring" --type=cpp 

# finds all text files not in an SCM dir ( recursively) and not a backup using type heuristics. 
ack -f --type=text  
Kent Fredric
  • 56,416
  • 14
  • 107
  • 150
  • Was not aware of either of those, is there a way to make either output directories? – user50264 Mar 18 '09 at 06:16
  • I like this solution for its brevity, however I think I might have a different version of sed than you, it replaces the first "s" is each directory with a colon. But kudos for making me upgrade to bash 4.0 :) – user50264 Mar 19 '09 at 00:48
  • O_o. Thats not a very complicated regex :S. \s\s* is supposed to just replace spaces. – Kent Fredric Mar 19 '09 at 03:54
  • @Kent - `\s` is PCRE syntax; it isn't supported in either basic _or_ extended POSIX REs, and thus not by standard sed. – Charles Duffy Feb 16 '11 at 07:17
  • @Charles : did you even test that assumption? What is your definition of "Standard Sed" ? Mine handles \s quite fine. `echo "hello world" | sed 's/\s/x/' ` emits `helloxworld` – Kent Fredric Feb 17 '11 at 06:10
  • bah, openbsd's sed is as you say, not `\s` friendly. use `[[:space:]]` instead of `\s` :( – Kent Fredric Feb 17 '11 at 06:16
  • @Kent - "standard" meaning conforming to POSIX without extensions. You know, the standard controlling the minimum functionality `sed` on UNIX is supposed to have. – Charles Duffy Feb 17 '11 at 09:31
  • Also, this answer is just *wrong* -- what happens if you have whitespace within your filenames? The following lacks that bug: `paths=( **/ ); oIFS="$IFS"; IFS=':'; CLASSPATH=${paths[*]}; IFS="$oIFS"` – Charles Duffy Feb 18 '11 at 02:43
1

I've been looking for a solution to this problem too. It would be great if bash had a way to say that for certain paths, you want it to search for the files recursively. For example

PATH="$PATH:/usr/local/bin:~/bin**"

where ~/bin would search that directory and all of its subdirectories without making a mess out of your PATH variable.

Since that's not implemented, my temporary solution is to put everything in my bin directory and then create another directory "bindir" that contains symbolic links to the actual executables in "bin", but their arranged neatly into subdirectories to make them easier to find.

My only question is whether I should hard links instead of symbolic links.

DaveShaw
  • 52,123
  • 16
  • 112
  • 141
Mark
  • 11
  • 1
0

Try this way:
export PATH="$PATH:$(du "~/code/" | cut -f2 | tr '\n' ':' | sed 's/:*$//')"
This will add ~/code itself along with all its subdirectories into $PATH
Explanation:
- du will display all the subdirectories info each line
- cut -f2 will extract the second column, i.e., the names of the subdirectories
- tr '\n' ':' will change each link break into a colon. This will join all lines into a single line and the subdirectories are delimited by a colon
- sed 's/:*$//' will remove the last colon

0

I have a single bin directory $HOME/bin and that gets an installed copy of any programs I build (or scripts, or symlinks to programs or scripts). It currently has almost 600 commands in it (ls | wc -l says 604, but there are a dozen or so sub-directories for various reasons).

When I'm testing a program, I execute it where I build it; once I've done testing for the time being, I acquire it with my acquire script, which copies the file and sets the permissions on it.

This leaves me with a nice tidy profile (I don't use .bashrc; I'd rather do the setup once when the login shell starts, and the sub-shells inherit the working environment without having to parse .bashrc again), but a rather ungainly bin directory. It avoids the cost of resetting PATH each time a shell starts, for example, and given how complex my path-setting code is, that is just as well!

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
0

Something like this:

_path="$(
  find <path> -name '.*' -prune -o -type d -print
  )" 

[[ $_path ]] && _path="${_path//$'\n'/:}" PATH="$PATH:${_path%:}"   

If you have GNU find you may use -printf ':%p' directly.

Paul Fleming
  • 24,238
  • 8
  • 76
  • 113
Dimitre Radoulov
  • 27,252
  • 4
  • 40
  • 48