1

I have a command (not echo!) that I want to run which takes an absolute path and a relative path.

How do I get these two arguments?

Attempt:

d=/tmp/foo;
find "$d" -type f -exec bash -c 'echo d=${1:${#d}} 1="${1%/*}"' bash {} \;

(I like GNU find because it's recursive, can restrict by file, can filter by filename, and doesn't spawn excessive shells)

Expectation:

mkdir -p /tmp/foo/bar/can/haz; touch /tmp/foo/bar/can/haz/bzr.txt
# cmd is run, output is:
d=bar/can/haz 1=/tmp/foo/bar/can/haz
1
  • and what should be the output if there would be file /tmp/foo/file.txt ? Commented Nov 23, 2017 at 12:56

1 Answer 1

6

Export d, then it will be available in your bash inline script. You also don't need bash at all here. Your (hopefully slimmer/faster) sh would do as well. Also, you don't need to run one shell per file. You can pass more files to your inline script with the -exec cmd {} + variant:

d=/tmp/foo
export d
find "$d" -type f -exec sh -c '
  for file do
    relative=${file#"$d/"}
    dir=${file%/*}
    relative_dir=${relative%/*}
    relative_dir=${relative_dir:-.}
    printf "%10s: %s\n" full "$file" \
                        relative "$relative" \
                        dir "$dir" \
                        reldir "$relative_dir"
  done' sh {} +

Which gives:

      full: /tmp/foo/bar/can/haz/bzr.txt
  relative: bar/can/haz/bzr.txt
       dir: /tmp/foo/bar/can/haz
    reldir: bar/can/haz

But if you only need the relative path, it may be simpler just to do:

(cd -P -- "$d" && find . -exec sh -c 'for file do...' sh {} +)

That would also make the command arguments passed to sh shorter so would allow find to pass more arguments to sh.

Note that there's nothing GNU-specific in your find command nor mine. That should work in any POSIX-compliant find implementation, not only the GNU one. The only non-POSIX part in your question was obviously bash and the ${1:offset} operator which is a Korn shell operator, not in POSIX sh.

For a recursive file lookup that allows you to specify the file type, see also zsh:

(cd -P -- "$d" &&
  for file (**/*(ND.)) {
    dir=$file:h
    printf '%10s: %s\n' relative $file reldir $dir
  })

Above, the . is the equivalent of find's -type f (only regular files), while D is to also include hidden ones like find does.


As a side note, in the general case:

c=$a$b; d=${c:${#a}}
[ "$b" = "$d" ] && echo yes

Is not guaranteed to output "yes", because the ${#var} and ${var:offset} operators work with characters, not bytes.

For instance in a UTF-8 locale, it would not output yes with these values of a and b:

a=$'St\xc3' b=$'\xa9phane'

With those, $c would contain my first name (Stéphane) $a contains half of that é character and $b the other half, ${#a} would be 3 (2 characters and 1 byte not forming a valid character but still counted).

So $d would be phane, not $'\xa9phane'.

In the specific case of d=$a/$b though, it should be OK, as none of the character set generally available in system locales would have a character other than / that contains the encoding of /.

1
  • If d is a relative value (assume ` . ` while pwd is /tmp/foo) the result of absolute paths gets distorted. Maybe d should be pre-transformed to absolute. Commented Nov 24, 2017 at 20:21

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.