Shellの基本を学ぶ（7）関数

# Function

シェルファイルのようなもの。パラメータを受け取れる。ポジショナルパラメータ `$1, $2...` も受け取れる。

構文は次の通り

* function name() {...} # オススメ
* function name {...}

ポイントとしては、名前の重複に気をつけること。例えば`ls`という Function を定義すると、もともとあったlsが使えなくなるので注意。

## サンプル1

引数も、戻り値も使える

```bash
#!/bin/bash

multiply() {
    return $(( $1 * $2 ))
}

multiply 4 5
echo $?
```

## 実行1

```bash
$ ./funcdemo
20
```

## サンプル２

ただし、この例では、特に戻り値はいらないので、省略してみる。

```bash
#!/bin/bash

multiply() {
    echo $(( $1 * $2 ))
}

multiply 4 5
```

結果は同じ

## サンプル3

次は、関数の戻り値がtrue or false で帰って来るので、それで判別している。関数の戻り値は、`$?` で取得しているのがポイント

```
#!/bin/bash

starts_with_b () {
    [[ $1 == [bB]* ]];
    return $?
}

result=$(starts_with_b Boy)
echo $?

if starts_with_b Boy; then
  echo "Start with B"
else
  echo "Not start with B"
fi

```

## 実行

```bash
$ ./funcdemo
0
Start with B
```

## ポイント

Function の中で定義された関数はローカルになる。
戻り値は、Exit ステータスコードにする。return ステートメントがなければ、Functionは最後のステートメントのステータスを返す

## 戻り値
スクリプトにグローバルな変数を使う
もしくは、Output ストリームに返す

## Export Functions

```bash
export -f fun 
```

# サンプルの数々

## box 

入力した文字の周りのボックスを作る。`${#1}` はパラメータの文字数のカウント

```bash
#!/bin/bash

drawline () {
    declare line=""
    declare char="-"
    for (( i=0; i<$1; ++i )); do
      line="${line}${char}"
    done
    printf "%s\n" "$line"
}

[[ ! $1 ]] && exit 0

declare -i len="${#1} + 4"
drawline $len
printf "| %s |\n" "$1"
drawline $len
```
## 実行

```bash
$ box hello
---------
| hello |
---------
```
## count 改造

ポイントは、ヒアドキュメントの書き方、それから、errorの関数は、エラー出力に吐くようになっている。標準出力もエラー出力に出力される書き方。

一つ謎なのが、getopts の `":hb:s:r"` の書き方。こうしないと動かない。最初は見間違いと思ったが。

```bash
#!/bin/bash

usage () {
    cat <<END
count [-r] [-b n] [-s n] stop

Print each number up to stop, beginning at 0
   -b: number to begin with (default: 0)
   -h: show this help message
   -r: reverses the count
   -s: sets step size (default: 1)
END
}

error() {
    echo "Error: $1"
    usage 
    exit $2
} >&2

isnum() {
    [[ $1 =~ ^[0-9]+$ ]]
}

declare reverse=""
declare -i start=0
declare -i step=1



while getopts ":hb:s:r" opt; do 
  case $opt in
    r)
      reverse="yes"
      ;;
    h)
      usage 
      exit 0
      ;;
    b)
      isnum ${OPTARG} || error "${OPTARG} is not number" 1
      start="${OPTARG}"
      ;;
    s)
      isnum ${OPTARG} || error "${OPTARG} is not number" 1
      step="${OPTARG}"
      ;;
    :)
      echo "Option -${OPTARG} is missing an argument"
      exit 1
      ;;
    \?)
      echo "Unknown option: -${OPTARG}" >&2
      exit 1
      ;;
  esac
done

shift $(( OPTIND -1 ))

[[ $1 ]] || { echo "Argument is missing" >&2; exit 1; }
declare end="$1"

if [[ ! $reverse ]]; then
  for (( i=start; i <= end; i+=step )); do
    echo $i
  done
else
  for (( i=end; i >= start; i-+step )); do
    echo $if
  done
fi

exit 0
```

## 実施結果


```bash
$ count -b 5 -s b 100
Error: b is not number
count [-r] [-b n] [-s n] stop

Print each number up to stop, beginning at 0
   -b: number to begin with (default: 0)
   -h: show this help message
   -r: reverses the count
   -s: sets step size (default: 1)
```

## read_pipe

Bash の初心者がやりがちな問題。次のプログラムは想定した通りに動かない。理由は、`$* | count_lines` の行で、`count_lines` が実行されるのは、サブプロセスだから、ここで定義された変数の数字をアップデート出来ない。ちなみに、`$*` はポジショナルパラメーターを展開して初めからスタートしたもの。

* [3.4.2 Special Parameters](https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html?fbclid=IwAR0PetqvIN0U5dMhKQuTVtV7h60soPBWXOnpskFiwYBTyDG7B1Goh24FrLk)

```bash
#!/bin/bash

declare -i count=0

count_lines () {
    while read -r; do 
      ((++count))
    done
    echo $count
}

$* | count_lines
echo $count
```

## 結果

```bash
$ read_pipe ls
9
0
```

## hist 

ディレクトリのファイルサイズと、パーセンテージを表す。ちなみに、bash の場所が違うのは、このシェルの実行には、V4 が必要で、Macは3xなので、インストールする必要があったため。

```bash
#!/usr/local/bin/bash

# This script prints a histrogram of how much space
# directories in the current working directory use

error () {
    echo "Error: $1"
    exit $2
} >&2

# Create a temp file.
my_mktemp () {
    mktemp || mktemp -t hist
} 2> /dev/null

echo $BASH_VERSINFO[0]
(( BASH_VERSINFO[0] < 4 )) && error "Bash 4+ required." 1

declare -A file_sizes
declare -r tempfile=$(my_mktemp) || error "Can not create templfile" 2

declare -ir term_cols=$(tput cols)

declare -i max_name_len=0 max_size=0 total_size=0

drawline () {
    declare line=""
    declare char="-"
    for (( i=0; i<$1; ++i )); do 
      line="${line}${char}"
    done
    printf "%s" "$line"
}

read_filesizes () {
    while read -r size name; do 
      file_sizes["$name"]="$size"
      (( total_size += size ))
      (( max_size < size )) && (( max_size=size ))
      (( max_file_len < ${#name} )) && (( max_file_len=${#name} ))
    done
}

{ du -d 0 */ || du --max-depth 0 *; } 2>/dev/null > "$tempfile"
read_filesizes < "$tempfile"

declare -i length percentage
declare -i cols="term_cols - max_file_len - 10"

echo "file_sizes${!file_sizez[@]}"

for k in "${!file_sizes[@]}"; do
  (( length=cols * file_sizes[$k] / max_size ))
  (( percentage=100 * file_sizes[$k] / total_size ))
  printf "%-${max_file_len}s | %3d%% | %s\n" "$k" "$percentage" $(drawline $length)
done

printf "%d Directories\n" "${#file_sizes[@]}"
printf "Total size: %d blocks\n" "$total_size"

rm "$tempfile"

```

## 結果

```bash
$ hist
5[0]
file_sizes
variables/ |   5% | -----------
file/      |   5% | -----------
forloop/   |  40% | ----------------------------------------------------------------------------------------
bin/       |  50% | ---------------------------------------------------------------------------------------------------------------
4 Directories
Total size: 160 blocks
```

# まとめ

* Function の直後にリダイレクトをすることが可能 `fun () {...} >&2`
* コマンドのパイプラインは、サブシェルで実行される `ls | while read -r; do ((++count)):done` 動作しない。ファイルを使う
* ヒアドキュメントは　`<< Tag ...Tag`