Как правильно обрабатывать и печатать файлы с пробелами в bash

Я пишу простую рекурсивную программу ls в bash (в которой я очень неопытен, так что не стесняйтесь быть жестоким).

Программа должна распечатывать каждый файл (возможно, каталог) в отдельной строке, и каждый раз, когда вводится новый каталог, вывод сдвигается на 4 пробела, чтобы получить древовидный вывод.

В настоящее время он неправильно распечатывает файлы с пробелами и не ставит косую черту после каталогов. (Подробнее ниже.)

Код

recls () {

    # store current working directory
    # issues: seems bad to have cwd defined up here and used down below in getAbsolutePath -- too much coupling
    cwd=$PWD
    # get absolute path of arg
    argdir=`getAbsolutePath "$@"`
    # check if it exists
    if [ ! -e $argdir ]; then
        echo "$argdir does not exist"
        return 1
    fi
    echo "$argdir exists"
    # check if it's a directory
    if [ ! -d $argdir ]; then
        echo "$argdir is not a directory"
        return 2
    fi
    echo "$argdir is a directory"
    tab=""
    recls_internal $argdir
    return 0

}

recls_internal () {

    for file in $@; do
        echo -n "$tab${file##/*/}"
        if [ -d $file ]; then
            # print forward slash to show it's a directory
            echo "/"
            savedtab=$tab
            tab="$tab    "
            myls_internal $file/*
            tab=$savedtab
        else
            # if not a directory, print a new line
            echo ""
        fi   
    done

}

getAbsolutePath () {

    if [ -z ${1##/*} ]; then
        echo "$1"
    else
        echo "$cwd/$1"
    fi

}

Выход

Сценарий находится в папке с именем bash-practice. Когда я делаю recls ., я получаю следующий вывод:

./
    myls.sh
    myls.sh~
    recdir.sh
    recls.sh
    recls.sh~
    sample
    document.txt
    sample-folder
        sample-stuff
            test-12.txt
        test-1.txt
        test-2.txt
        sort-test.txt
        sort-text-copy.txt
        test-5-19-14-1

Проблема

Как видите, отступ работает правильно, но есть две проблемы:

1) Файл sample document.txt разбит на две строки, потому что в нем есть пробел.

2) Каждый каталог должен иметь перед собой косую черту, но по какой-то причине это работает только для самого первого.

Попытка решения

Чтобы исправить (1), я попытался сохранить внутренний разделитель файлов и заменить его символом новой строки следующим образом:

...
tab=""
savedIFS=$IFS
IFS="\n"
recls_internal $argdir
IFS=$savedIFS
return 0

Но это совсем не сработало. Он даже не отображал больше, чем первая папка. Ясно, что мое понимание вещей неверно.

Что касается (2), я не вижу причин, по которым он не должен работать должным образом.

Вывод

bash сложен для меня, так как он, кажется, имеет более необычный синтаксис, чем большинство других языков программирования (язык сценариев оболочки), поэтому я был бы признателен за любое понимание моих ошибок, а также за решение.

Обновление №1

Я зашел на сайт http://www.shellcheck.com, который предложил mklement0, и практически все его подсказки двойные кавычки. Когда я дважды процитировал "$@", программа правильно напечатала файл sample document.txt, но сразу после этого выдала мне ошибку "binary operator expected". Вот распечатка того, как это выглядит сейчас:

введите здесь описание изображения

Обновление №2 [проблема решена?]

Хорошо, оказалось, что у меня была опечатка, из-за которой по умолчанию использовалась более ранняя версия моей функции с именем myls_internal, когда она рекурсивно. Эта более ранняя версия не помечала каталоги косой чертой. Также было исправлено сообщение об ошибке в разделе «Обновление». я изменил строку

myls_internal "$file/*"

to

recls_internal $file/*

и теперь он, кажется, работает правильно. Если кто-то находится в процессе написания ответа, я все равно ценю ваше понимание, поскольку я не совсем понимаю механизм того, как цитирование «$ @» решило проблему с пробелами.

Фиксированный код:

recls () {

    # store current working directory
    # issues: seems bad to have cwd defined up here and used down below in getAbsolutePath -- too much coupling
    cwd=$PWD
    # get absolute path of arg
    argdir=$(getAbsolutePath "$@")
    # check if it exists
    if [ ! -e $argdir ]; then
        echo "$argdir does not exist"
        return 1
    fi
    echo "$argdir exists"
    # check if it's a directory
    if [ ! -d $argdir ]; then
        echo "$argdir is not a directory"
        return 2
    fi
    echo "$argdir is a directory"
    tab=""
    recls_internal $argdir
    return 0

}

recls_internal () {

    for file in "$@"; do
        echo -n "$tab${file##/*/}"
        if [ -d "$file" ]; then
            # print forward slash to show it's a directory
            echo "/"
            savedtab=$tab
            tab="$tab    "
            recls_internal $file/*
            tab=$savedtab
        else
            # if not a directory, print a new line
            echo ""
        fi   
    done

}

getAbsolutePath () {

    if [ -z ${1##/*} ]; then
        echo "$1"
    else
        echo "$cwd/$1"
    fi

}

Фиксированный вывод:

введите здесь описание изображения

Обновление №3

Линия

recls_internal $file/*

вместо этого должно быть

recls_internal "$file"/*

который правильно обрабатывает каталоги с пробелами в них. В противном случае такая папка, как cs 350, содержащая Homework1.pdf и Homework2.pdf, расширится до

cs 350/Homework1.pdf 350/Homework2.pdf

когда это должно быть

cs 350/Homework1.pdf cs 350/Homework2.pdf

Я думаю? Я действительно не понимаю более тонких деталей того, что происходит, но это, казалось, исправило это.


person Chris Middleton    schedule 20.05.2014    source источник
comment
@rpax Не ожидал, что кто-то это скажет, спасибо!   -  person Chris Middleton    schedule 20.05.2014
comment
Вставьте свой код на shellcheck.net — вы получите много советов по повторному цитированию и нежелательному разделению слов.   -  person mklement0    schedule 20.05.2014
comment
@ mklement0 Спасибо, я посмотрю на это. И если кто-то еще столкнется с этой проблемой, я просто исправил проблемы с подсветкой синтаксиса в своем посте, явно указав язык. См. здесь: meta.stackexchange.com/questions /981/   -  person Chris Middleton    schedule 20.05.2014
comment
Это полезный совет относительно подсветки синтаксиса, но обратите внимание, что официальный пост на эту тему теперь находится по адресу meta.stackexchange.com/questions/184108/   -  person mklement0    schedule 20.05.2014
comment
@Bill Я на самом деле не разбираю ls. Вы увидите, что я никогда не вызываю ничего, кроме echo и моей собственной функции recls. И все же спасибо за ссылку.   -  person Chris Middleton    schedule 20.05.2014
comment
Одна часть ответа заключается в том, чтобы при расширении имена файлов и пути заключались в двойные кавычки. Есть места, где вы можете обойтись без использования кавычек, но вы можете «всегда» использовать кавычки и получить тот же результат. («Всегда» в кавычках, потому что существует исключение для операции регулярного выражения встроенной команды [[ ... ]].)   -  person Jonathan Leffler    schedule 20.05.2014
comment
Если и только если вы на самом деле не хотите писать это самостоятельно, есть дерево.   -  person Biffen    schedule 20.05.2014
comment
Я только что отредактировал свой пост с обновлением, которое, похоже, решило мою проблему. Если кто-то все еще хочет дать ответ, я был бы признателен за понимание.   -  person Chris Middleton    schedule 20.05.2014


Ответы (1)


Чтобы проиллюстрировать разницу между "$@" и $@, рассмотрим две следующие функции:

f() { for i in $@; do echo $i; done; }

g() { for i in "$@"; do echo $i; done; }

При вызове этих функций с параметрами a "b c" "d e" результат будет

  • функция f

f a "b c" "d e" a b c d e

  • функция г g a "b c" "d e" a b c d e

Таким образом, когда "$@" находится в двойных кавычках, расширение сохраняет каждый параметр в отдельном слове (даже если параметр содержит один или несколько пробелов). При расширении $@ (без двойных кавычек) параметр с пробелом будет рассматриваться как два слова.

В вашем скрипте вам также нужно заключить argdir и file в двойные кавычки. Это полезно, когда имя каталога или файла содержит пробел, поэтому имя будет рассматриваться как одно значение. Ниже ваш скрипт изменен.

#! /bin/bash -u
recls () {

    # store current working directory
    # issues: seems bad to have cwd defined up here and used down below in getAbsolutePath -- too much coupling
    cwd=$PWD
    # get absolute path of arg
    argdir=`getAbsolutePath "$@"`
    # check if it exists
    if [ ! -e "$argdir" ]; then
        echo "$argdir does not exist"
        return 1
    fi
    echo "$argdir exists"
    # check if it's a directory
    if [ ! -d "$argdir" ]; then
        echo "$argdir is not a directory"
        return 2
    fi
    echo "$argdir is a directory"
    tab=""
    recls_internal "$argdir"
    return 0

}

recls_internal () {

    for file in "$@"; do
        echo -n "$tab${file##/*/}"
        if [ -d "$file" ]; then
            # print forward slash to show it's a directory
            echo "/"
            savedtab=$tab
            tab="$tab    "
            recls_internal "$file"/*
            tab=$savedtab
        else
            # if not a directory, print a new line
            echo ""
        fi   
    done

}

getAbsolutePath () {

    if [ -z ${1##/*} ]; then
        echo "$1"
    else
        echo "$cwd/$1"
    fi

}
person el aurens    schedule 20.05.2014
comment
Спасибо за Ваш ответ. Это проясняет часть моего замешательства. Я все еще немного запутался в общем, но я нашел это мета-объяснение семантики bash, которое может мне помочь: stackoverflow.com/questions/23207168/. - person Chris Middleton; 21.05.2014
comment
@AmadeusDrZaius, пожалуйста. Спасибо за редактирование - person el aurens; 21.05.2014