シェルスクリプトでパーミッションの記号形式と数字形式を相互変換する

## はじめに

シェルスクリプトでパーミッションの記号形式（例 `rw-r--r--`）と数字形式（例 `0644`）を相互に変換する関数を作ってみました。`ls -l` コマンドの出力のパーミッションを `chmod` コマンドの引数に使ったりすることが出来ます。例のごとく（？）外部コマンドなしの完全なシェルスクリプト実装かつ、厳密に POSIX に準拠しています。

### 実装

#### 記号形式から数字形式へ

```sh
perm_sym2num() {
  perm=$1 p=0 q=0 r='' i=4
  while [ "$perm" ]; do
    case $perm in (r??*) p=$((p + 4)); esac
    case $perm in (?w?*) p=$((p + 2)); esac
    case $perm in (??[xst]*) p=$((p + 1)); esac
    case $perm in (??[sStT]*) q=$((q + i)); esac
    perm=${perm#???} r=${r}${p} && p=0 i=$((i / 2))
  done
  echo "${q}${r}"
}

perm_sym2num "rwsr-xr-x" # => 4755
```

#### 数字形式から記号形式へ

```sh
perm_num2sym() {
  [ ${#1} = 3 ] && set -- "0$1"
  q=${1%???} perm=${1#?} i=4 r=''
  while [ "$perm" ] && p=${perm%"${perm#?}"}; do
    [ $((p & 4)) = 0 ] && r="${r}-" || r="${r}r"
    [ $((p & 2)) = 0 ] && r="${r}-" || r="${r}w"
    case $((p & 1))$i in
      01) [ $((q & i)) = 0 ] && r="${r}-" || r="${r}T" ;;
      ?1) [ $((q & i)) = 0 ] && r="${r}x" || r="${r}t" ;;
      0?) [ $((q & i)) = 0 ] && r="${r}-" || r="${r}S" ;;
      ??) [ $((q & i)) = 0 ] && r="${r}x" || r="${r}s" ;;
    esac
    q=$((q % i)) perm=${perm#?} && i=$((i / 2))
  done
  echo "$r"
}

perm_num2sym "4755" # => rwsr-xr-x
```

### 発展

例のごとく（？）ローカル変数の代わりに位置パラメータを使い、値を標準出力ではなく指定した変数に返すバージョンの実装です。可読性は落ちますが変数がかぶることはなく、変換後の値を変数にキャプチャするのにコマンド置換（サブシェル）が不要になるので、速度が向上します。

#### 記号形式から数字形式へ

```sh
perm_sym2num() {
  set -- "$1" "$2" 0 0 '' 4
  while [ "$2" ]; do
    case $2 in (r??*) set -- "$1" "$2" $(($3 + 4)) "$4" "$5" "$6"; esac
    case $2 in (?w?*) set -- "$1" "$2" $(($3 + 2)) "$4" "$5" "$6"; esac
    case $2 in (??[xst]*) set -- "$1" "$2" $(($3 + 1)) "$4" "$5" "$6"; esac
    case $2 in (??[sStT]*) set -- "$1" "$2" "$3" $(($4 + $6)) "$5" "$6"; esac
    set -- "$1" "${2#???}" 0 "$4" "${5}${3}" $(($6 / 2))
  done
  eval "$1=\${4}\${5}"
}

perm_sym2num ret "rwsr-xr-x"
echo "$ret" # => 4755
```

#### 数字形式から記号形式へ

```sh
perm_num2sym() {
  [ ${#2} = 3 ] && set -- "$1" "0$2"
  set -- "$1" "${2%???}" "${2#?}" 4 ''
  while [ "$3" ] && set -- "$@" ${3%"${3#?}"}; do
    [ $(($6 & 4)) = 0 ] && set -- "$@" - || set -- "$@" r
    [ $(($6 & 2)) = 0 ] && set -- "$@" - || set -- "$@" w
    case "$(($6 & 1))$4" in
      01) [ $(($2 & $4)) = 0 ] && set -- "$@" - || set -- "$@" T ;;
      ?1) [ $(($2 & $4)) = 0 ] && set -- "$@" x || set -- "$@" t ;;
      0?) [ $(($2 & $4)) = 0 ] && set -- "$@" - || set -- "$@" S ;;
      ??) [ $(($2 & $4)) = 0 ] && set -- "$@" x || set -- "$@" s ;;
    esac
    set -- "$1" $(($2 % $4)) "${3#?}" $(($4 / 2)) "${5}${7}${8}${9}"
  done
  eval "$1=\$5"
}

perm_num2sym ret "4755"
echo "$ret" # => rwsr-xr-x
```

## おまけ `ls -l` のパーミッション部分を取得する方法

```sh
perm=$(ls -l test.txt) && perm=${perm#?} && perm=${perm%"${perm#?????????}"}

# 補足 パーミッション部分は -rw-r--r--+ や -rw-r--r--@ のように
# 末尾に文字が追加されることがある（この表記は POSIX でも規定されている）ので
# 最初の 1 文字を除いた、次の 9 文字を切り出している

# よく使うようであれば関数にしておくと良い
get_perm() {
  eval "$1=\$(ls -l \"\$2\") && $1=\${$1#?} && $1=\${$1%\"\${$1#?????????}\"}"
}
get_perm perm "test.txt"
echo "$perm" # => rw-rw-r--
```

または、以下など

```sh
perm=$(ls -l test.txt) && perm=$(expr "$perm" : '.\(.........\)')
perm=$(ls -l test.txt) && perm=$(echo "$perm" | awk '{print substr($0,2,9)}')
```

**注意**

下記のコードは一見うまくいくように見えるがファイルが見つからない場合にエラーにならないので注意。

```sh
perm=$(ls -l test.txt | awk '{print substr($0,2,9)}')
echo $? # ファイルが見つからなくても 0 になる
```

`set -o pipefail` （POSIX Issue8 より標準化される予定）を実行しておくか `perm` 変数の中があるか調べる必要がある。

```sh
perm=$(set -o pipefail; ls -l test.txt | awk '{print substr($0,2,9)}')

perm=$(ls -l test.txt | awk '{print substr($0,2,9)}') && [ "$perm" ]
```

## さいごに

この記事のコードのライセンスは CC0 とします。自由にコピーしたり、独自の修正を加えたりして使用して構いません。