シェルスクリプトで動作するコマンドラインインタフェース

シェルスクリプトで動作するコマンドラインインタフェース

Posted at February 12,2020 11:55 PM
Tag:[bash, Linux]

シェルスクリプトで動作するコマンドラインインタフェースを作りました。

1.はじめに

Linuxにログインしたターミナル上では、標準入力した文字列(コマンドライン)を編集することができます。また矢印キーでコマンド履歴を表示・編集することもできます。

が、シェルスクリプトを起動したあとの標準入力を、同じように編集したり、コマンド履歴を表示することはできません。

ネットを探しましたが同じようなものがみつかりませんでした。

ということで、シェルスクリプト上でコマンドラインを編集できるスクリプトを作ってみました。

このスクリプトでできるコマンドラインインタフェースは下記です。

  • Ctrl+a:カーソルを先頭に移動
  • Ctrl+b:カーソルを1文字左に移動
  • Ctrl+d:カーソル上の文字を削除
  • Ctrl+e:カーソルを末尾に移動
  • Ctrl+f:カーソルを1文字右に移動
  • Ctrl+k:カーソルより後方の文字を削除
  • Ctrl+u:カーソルより前方の文字を削除
  • Backspace:カーソルの左側の文字を削除
  • 上矢印:ヒストリ呼び出し
  • 下矢印:ヒストリ呼び出し
  • 左矢印:カーソルを1文字左に移動
  • 右矢印:カーソルを1文字右に移動
  • リターン:コマンド実行

スクリプトを任意の名称で保存して実行すると、スクリプトで定義したプロンプトを表示して標準入力待ちになります。

ご自身のスクリプトに合うよう、適当に組み込んでください。

不具合・改善等があれば適宜アップデートしていきます。

CentOS 7で動作確認しています。環境によっては期待通り動作しない可能性があります。予めご了承ください。

2.スクリプト

#!/bin/bash
 
# Copyright (c) 2020 www.koikikukan.com
 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom
# the Software is furnished to do so, subject to the following
# conditions:
 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
 
history -r /root/.bash_history
 
# プロンプト
PROMPT="# "
 
PROMPT_LENGTH=${#PROMPT}
CURSOL_INIT_POS=$((PROMPT_LENGTH + 1))
COMMAND=''
LENGTH=0
COUNTER=0
POS=${CURSOL_INIT_POS}
 
function _history_update() {
    tput cub 1000
    tput dl 1
    COMMAND=`history | tail -$COUNTER | head -1 | sed -re "s/^\s+[0-9]+\s{2}//"`
    echo -n "${PROMPT}$COMMAND"
    LENGTH=${#COMMAND}
    POS=$((LENGTH + CURSOL_INIT_POS))
}
 
echo -n "${PROMPT}"
while IFS= read -r -n1 -s char; do
    case $char in
    $'\x1b')
        read -r -n2 -s rest
        char+="$rest"
        case $char in
        $'\033\x5b\x41')
            HISTORY_COUNT=`history|wc -l`
            if [ $COUNTER -le $HISTORY_COUNT ]; then
                COUNTER=$((COUNTER + 1))
                _history_update
            fi
        ;;
        $'\033\x5b\x42')
            if [ $COUNTER -gt 0 ]; then
                COUNTER=$((COUNTER - 1))
                _history_update
            fi
        ;;
        $'\033\x5b\x43')
            COMMAND_POS=$((LENGTH + PROMPT_LENGTH))
            if [ ${COMMAND_POS} -ge ${POS} ]; then
                tput cuf 1
                POS=$((POS + 1))
            fi
        ;;
        $'\033\x5b\x44')
            if [ ${POS} -gt ${CURSOL_INIT_POS} ]; then
                tput cub 1
                POS=$((POS - 1))
            fi
        ;;
    esac
    ;;
 
    $'\x08')
        if [ ${POS} -gt ${CURSOL_INIT_POS} ]; then
            LEFT_LENGTH=$((POS - CURSOL_INIT_POS))
            if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
                TMP=$((LEFT_LENGTH - 1))
                COMMAND=`echo "${COMMAND}" | sed -e "s/^\(.\{${TMP}\}\).\(.*\)$/\1\2/"`
            else
                DEL=${COMMAND: -1}
                COMMAND="${COMMAND/%${DEL}/}"
            fi
            tput dl 1
            tput cub 1000
            echo -n "${PROMPT}${COMMAND}"
            if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
                tput cub 1000
                POS2=$((POS - 2))
                tput cuf ${POS2}
            fi
            POS=$((POS - 1))
            LENGTH=$((LENGTH - 1))
        fi
    ;;
 
    $'\0a')
        history -s "${COMMAND}"
        echo
        if [ -n "$COMMAND" ];then
            eval "${COMMAND}"
        fi
        LENGTH=0
        COMMAND=''
        POS=$CURSOL_INIT_POS
        echo -n "${PROMPT}"
    ;;
 
    $'\ca')
        if [ ${POS} -gt ${CURSOL_INIT_POS} ]; then
            tput cub 1000
            tput cuf ${PROMPT_LENGTH}
            POS=$CURSOL_INIT_POS
        fi
    ;;
 
    $'\cb')
        if [ ${POS} -gt ${CURSOL_INIT_POS} ]; then
            tput cub 1
            POS=$((POS - 1))
        fi
    ;;
 
    $'\cd')
        TMP=$((LENGTH + PROMPT_LENGTH))
        if [ ${POS} -le ${TMP} ]; then
            LEFT_LENGTH=$((POS - CURSOL_INIT_POS))
            if [ ${POS} -ne ${CURSOL_INIT_POS} ]; then
                COMMAND=`echo "${COMMAND}" | sed -e "s/^\(.\{${LEFT_LENGTH}\}\).\(.*\)$/\1\2/"`
            else
                COMMAND="${COMMAND:1}"
            fi
            tput dl 1
            tput cub 1000
            echo -n "${PROMPT}${COMMAND}"
            if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
                tput cub 1000
                POS2=$((POS - 1))
                tput cuf ${POS2}
            fi
            LENGTH=$((LENGTH - 1))
        fi
    ;;
 
    $'\ce')
        COMMAND_POS=$((LENGTH + PROMPT_LENGTH))
        if [ ${COMMAND_POS} -ge ${POS} ]; then
            tput cub 1000
            tput cuf $COMMAND_POS
            POS=$((COMMAND_POS+1))
        fi
    ;;
 
    $'\cf')
        COMMAND_POS=$((LENGTH + PROMPT_LENGTH))
        if [ ${COMMAND_POS} -ge ${POS} ]; then
            tput cuf 1
            POS=$((POS + 1))
        fi
    ;;
 
    $'\ck')
        LEFT_LENGTH=$((POS - CURSOL_INIT_POS))
        if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
            COMMAND=`echo "${COMMAND}" | sed -e "s/^\(.\{${LEFT_LENGTH}\}\).*$/\1/"`
        else
            continue
        fi
        tput dl 1
        tput cub 1000
        echo -n "${PROMPT}${COMMAND}"
        LENGTH=$((POS - CURSOL_INIT_POS))
    ;;
 
    $'\cu')
        LEFT_LENGTH=$((POS - CURSOL_INIT_POS))
        RIGHT_LENGTH=$((LENGTH - LEFT_LENGTH))
        if [ ${POS} -gt ${CURSOL_INIT_POS} ]; then
            COMMAND=`echo "${COMMAND}" | sed -e "s/^.*\(.\{${RIGHT_LENGTH}\}\)$/\1/"`
        else
            continue
        fi
        tput dl 1
        tput cub 1000
        echo -n "${PROMPT}${COMMAND}"
        tput cub 1000
        tput cuf $PROMPT_LENGTH
        POS=$CURSOL_INIT_POS
        LENGTH=$((RIGHT_LENGTH))
    ;;
 
    *)
        LEFT_LENGTH=$((POS - CURSOL_INIT_POS))
        if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
            COMMAND=`echo "${COMMAND}" | sed -e "s/^\(.\{${LEFT_LENGTH}\}\)/\1$char/"`
        else
            COMMAND="${COMMAND}$char"
        fi
        tput dl 1
        tput cub 1000
        echo -n "${PROMPT}${COMMAND}"
        if [ ${LENGTH} -gt ${LEFT_LENGTH} ]; then
            tput cub 1000
            tput cuf ${POS}
        fi
        POS=$((POS + 1))
        LENGTH=$((LENGTH + 1))
    ;;
    esac
done

3.ライセンス

本スクリプトはMITライセンスです。

関連記事
zenback
人気エントリー
トラックバックURL


コメントする
greeting

*必須

*必須(非表示)


ご質問のコメントの回答については、内容あるいは多忙の場合、1週間以上かかる場合があります。また、すべてのご質問にはお答えできない可能性があります。予めご了承ください。

太字イタリックアンダーラインハイパーリンク引用
[サインインしない場合はここにCAPTCHAを表示します]

コメント投稿後にScript Errorや500エラーが表示された場合は、すぐに再送信せず、ブラウザの「戻る」ボタンで一旦エントリーのページに戻り(プレビュー画面で投稿した場合は、投稿内容をマウスコピーしてからエントリーのページに戻り)、ブラウザをリロードして投稿コメントが反映されていることを確認してください。

コメント欄に(X)HTMLタグやMTタグを記述される場合、「<」は「&lt;」、「>」は「&gt;」と入力してください。例えば「<$MTBlogURL$>」は「&lt;$MTBlogURL$&gt;」となります(全て半角文字)