Shellの基本を学ぶ（8）String の操作

# Parameter Expansion

* `${#var}`: `$var` の長さ

# String の一部を取り除く

* `${var#pattern}` 文字列の初めからもっともショートマッチした部分を削除
* `${var##pattern}` 文字列の初めから、もっとロングマッチした部分を削除
* `${var%pattern}` 文字列の最後からもっともショートマッチした部分を削除
* `${var%%pattern}` 文字列の最後からもっともロングマッチした部分を削除

`*, ?, []` などもブラケットの中で使用可能。

Example i="/Users/Tsuyoshi/demo.txt"

`${i#*/} Users/Tsuyoshi/demo.txt`
`${i##*/} demo.txt`
`${i%.*} /Users/Tsuyoshi/demo`
`${i%/*} /Users/Tsuyoshi`

# Search と Replace

* `${var/pattern/string} 最初のマッチを置き換える`
* `${var//pattern/string} 全てのマッチを置き換える`
* `${var/#pattern/string} 最初の文字を置き換える`
* `${var/%pattern/string} 最後からマッチしたものを置き換える`

## demo

```bash
$i="myjpg.jpg"
$echo ${i/jpg/txt}
mytxt.jpg
$echo ${i/%jpg/txt}
myjpg.txt
$echo ${i//jpg/txt}
mytxt.txt
$echo ${i//jpg}
my.
$echo ${i/%jpg/}
myjpg.
$echo ${i#jpg}
myjpg.jpg  # String がjpg で始まっていないため。
$echo ${i/[yj]/a}
majpg.jpg
$echo ${i//[yj]/a}
maapg.apg
```
_mvext_

```bash
#!/bin/bash

[[ $# -ne 2 ]] && { echo "Missing two arguments" >&2; exit 1; } 

for f in *"$1"; do
  mv "$f" "${f/%$1/$2}"
done
```

## 実行

```bash
$ls
a.jpg	b.jpg	c.jpg
$mvext jpg txt
$ls
a.txt	b.txt	c.txt
```

# Default value

`${var:-value}`
`${var-value}` var 空のケースに限る　（上記の省略系）
`${var:=value}` varがカラだったり、セットされていないケースにアサインする
`${var=value}` var がunsetの時のみ

* [Variable name expansion](https://wiki.bash-hackers.org/syntax/pe#variable_name_expansion)


## サンプル

次の例は`$NOTESDIR` がある場合は、その値、なければ `$HOME`が使われる。

```bash
declare notesdir=${NOTESDIR:-$HOME}
```
# コンディショナル

## Pattern matching

* `[[ .. ]]` の中の `==` `!=` はパターンマッチングを行う
* `[[ $var == pattern ]]` は `$var` がパターンにマッチすると true を返す
* パターンマッチは、`*, ? , []` などを用いる
* ダブルクオートで囲むと、パターンマッチではなく、文字列として比較を行う

## サンプル

パターンマッチが上記の通り行われている。ダブルクオートだと、パターンマッチではなく、文字列そのものとしてマッチする。最後のものは、間違えて`[ .. ]` にしたもの。この場合、`t*i` は、ファイル名として解釈される。

```bash
$[[ tsuyoshi = t*i ]] && echo yep
yep
$[[ tsuyoshi = "t*i" ]] && echo yep
$[[ "t*i" = "t*i" ]] && echo yep
yep
$[ tsuyoshi = t*i ] && yep
$ls
a.txt		box		count		d.txt		hw		read_pipe
b.txt		c.txt		create_script	hist		mvext		test
$echo [ m*t ]
[ mvext ]
$
```

# Regular Expression 

正規表現

* `=~`正規表現のマッチ

## `?`
`?` はトークンが0もしくは1回の出現にマッチする。`[0-9]?` は１文字の数字もしくは、何もない変数にマッチする。

## `*`
`*` は全てのトークンにマッチする。`[a-z]*` は全ての小文字の英字もしくは、何もないのにマッチする。

## `+`
`+`は１回以上にマッチする。`[0-9]+`は、１回以上の数字が出て来ることにマッチする

## `^`
文字の開始にマッチする

## `$`
文字の終了にマッチする

## `.`
任意の一文字

# サンプル

以前のカウントを改造する。`isnum`メソッドを改造してパターンマッチを使う。ポイントとしては、最初に、変数に正規表現を代入して使っている。シングルクオートを使っている。これは、エスケープをするのが面倒なので、シングルクオートを使って、変数に代入することでそれを避けられるから、コードが楽になる。

`^[0-9]+$`というのは、先頭文字`^` から、数字`[0-9]`何文字か`+`最後まで`$` という意味なので、任意の数字にヒットする。`^0(.+)`というのは、先頭`^`が0その後続は任意の文字（複数）`(.+)` という意味。つまり先頭が0でそのあとは任意。数字チェックをしたif の中にいるので、当然数字で0始まり、つまり、８進数表現を表している。（Bashの場合)。

ちなみに、`$BASH_REMATCH` は正規表現にマッチした内容が配列になって入っているもの。`$BASH_REMATCH[0]` は全体で、`$BASH_REMATCH[1]` 以降はマッチの部分が入っていく。この場合は、0を除いた数字が入る。

* [Regular Expression Matching (REMATCH)](http://molk.ch/tips/gnu/bash/rematch.html)

```bash
isnum() {
    declare -r num_re='^[0-9]+$'
    declare -r octal_re='^0(.+)'
    num_error="ok"
    if [[ $1 =~ $num_re ]]; then
      if [[ $1 =~ $octal_re ]]; then
        num_error="$1 is not a number, did you mean ${BASH_REMATCH[1]}?"
        return 1
      fi
    else 
      num_error="$1 is not a number"
      return 1
    fi

    return 0
}
```

## 実行

実行するとちゃんと８進数が弾かれる。０も、`^0(.+)` で`+`が一文字以上なので、マッチしないので問題ない。

```bash
$count 020
Error: 020 is not a number, did you mean 20?
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)
$count 0
0
```

## 全てのコード

_count_

```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() {
    declare -r num_re='^[0-9]+$'
    declare -r octal_re='^0(.+)'
    num_error="ok"
    if [[ $1 =~ $num_re ]]; then
      if [[ $1 =~ $octal_re ]]; then
        num_error="$1 is not a number, did you mean ${BASH_REMATCH[1]}?"
        return 1
      fi
    else 
      num_error="$1 is not a number"
      return 1
    fi

    return 0
}

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



while getopts ":hb:s:r" opt; do 
echo "OPTARG: ${OPTARG}"
  case $opt in
    r)
      reverse="yes"
      ;;
    h)
      usage 
      exit 0
      ;;
    b)
      isnum ${OPTARG} || error "${num_error}" 1
      start="${OPTARG}"
      ;;
    s)
      isnum ${OPTARG} || error "${num_error}" 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"

isnum $1 || { error "${num_error}" 1 ; exit 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

```

# End of options

`--` この後の引数は、評価されない。例えば`-l.txt` というファイルがあるとすると、引数と勘違いされる。`rm -- -l.txt` とやるとうまくいく。そういったファイル名を含む場合に、ファイル操作したい場合、

```bash
$ for in *.txt; do touch -- $i; done
```
と言った書き方もできる。引数としての変数にを使うときに、使うのは良い習慣。引数は、自分でコントロールできないので、コマンドを呼ぶときに、`--` を使う

# サンプル

`-a.txt` というファイル名は通常作れませんが、`--`を使うと、可能になります。

```bash
$touch -a.txt
touch: illegal option -- .
usage:
touch [-A [-][[hh]mm]SS] [-acfhm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...
$touch -- -a.txt
$ls
-a.txt	a.txt	b.txt	c.txt
```

既存のプログラム`mvext`を動作させてみますが、このようなファイル名が含まれている場合は動きません。

```bash
$mvext txt jpg
mv: illegal option -- a
usage: mv [-f | -i | -n] [-v] source target
       mv [-f | -i | -n] [-v] source ... directory
```


出来るように改造してみます。`mv` の所に `--`をつけて、パラメータと思われないようにします。

_mvext_

```bash
#!/bin/bash

[[ $# -ne 2 ]] && { echo "Missing two arguments" >&2; exit 1; } 

for f in *"$1"; do
  mv -- "$f" "${f/%$1/$2}"
done
```

すると

```bash
$mvext txt jpg
$ls
-a.jpg	a.jpg	b.jpg	c.jpg
```

綺麗に動作します。他にも、パラメータではない部分には、入れといたほうが良さそうです。

_tn_

```bash
#!/bin/bash

declare -r date=$(date)

declare -r topic="$1"

declare notesdir=${NOTESDIR:-$HOME}

if [[ ! -d $notesdir ]]; then
  mkdir -- "${notesdir}" 2> /dev/null || { echo "Cannot make directory ${notesdir}" 1>&2 ; exit 1; }
fi

declare -f filename="${notesdir}/${topic}notes.txt"
if [[ ! -f $filename ]]; then
  touch -- "${filename}" 2> /dev/null || { echo "Cannot create file ${filename}." 1>&2 ; exit 1;}
fi

[[ -w $filename ]] || { echo "${filename} is not writable" 1>&2; exit 1; }

read -p "Your note: " note

if [[ $note ]]; then
  echo "$date: $note" >> "$filename"
  echo "Note '$note' saved to $filename" 1>&2
else
  echo "No input; note wasn't saved." 1>&2
  exit 2
fi

exit 0
```